App development

UIDocument From Scratch [FREE]

Replace word: Lea Marolt Sonnenschein up to date this tutorial for iOS 13, Xcode 11 and Swift 5. Ray Wenderlich wrote the unique.

There are just a few methods to retailer information within the iOS ecosystem:

UserDefaults for small quantities of information.

Core Knowledge for big quantities of information.

UIDocuments once you base your app across the idea of particular person paperwork the consumer can create, learn, replace and delete.

The iOS 11 additions of UIDocumentBrowserViewController and the Recordsdata app have made life considerably less complicated by offering easy accessibility to handle recordsdata in apps. However what in case you wished extra granular management?

On this tutorial, you’ll learn to create, retrieve, edit and delete UIDocuments to the iOS file system from scratch. This covers 4 matters:

Creating information fashions.
Subclassing UIDocument.
Creating and itemizing UIDocuments.
Updating and deleting UIDocuments.

Getting Began

On this tutorial, you’ll create an app referred to as PhotoKeeper, which lets you retailer and title your favourite images. Use the Obtain Supplies button on the high or backside of this tutorial to obtain the starter venture.

Open the starter venture. Then, construct and run.

You possibly can add entries to a desk view by tapping the + button on the correct and edit them by tapping the Edit button on the left.

The app you’ll find yourself with will will let you choose and title your favourite images. You’ll additionally have the ability to change the picture or title or delete it totally.

Knowledge Fashions

UIDocument helps two totally different courses for enter/output:

Knowledge: A easy information buffer. Use this when your doc is a single file.

FileWrapper: A listing of file packages which the OS treats as a single file. It’s nice when your doc consists of a number of recordsdata you wish to load independently.

The info mannequin for this tutorial is sort of easy: It’s only a picture! So, it might sound that utilizing Knowledge would take advantage of sense.

Nonetheless, you wish to present a thumbnail of a photograph within the grasp view controller earlier than the consumer opens a file. If you happen to used Knowledge, you’d should open and decode each single doc from the disk to get the thumbnails. For the reason that pictures may be fairly massive, this might result in sluggish efficiency and excessive reminiscence overhead.

So, you’re going to make use of FileWrapper as a substitute. You’ll retailer two paperwork contained in the wrapper:

PhotoData represents the full-size picture.

PhotoMetadata represents the picture thumbnail. It’s a small quantity of information that the app can load rapidly.

First, outline some constants. Open Doc.swift and add this on the high of the doc, proper after import UIKit:

extension String

Have in mind:

“ptk” is your app’s particular file extension, so you may determine the listing as a doc your app is aware of deal with.

“Model” is the important thing to encode and decode the file’s model quantity so you may replace the info construction if you’d like assist older recordsdata sooner or later.

“Picture” and “Thumbnail” are keys for NSCoding.

Now open PhotoData.swift and implement the PhotoData class:

class PhotoData: NSObject, NSCoding
var picture: UIImage?

init(picture: UIImage? = nil)
self.picture = picture

func encode(with aCoder: NSCoder)

required init?(coder aDecoder: NSCoder)
aDecoder.decodeInteger(forKey: .versionKey)
guard let photoData = aDecoder.decodeObject(forKey: .photoKey) as? Knowledge else
return nil

self.picture = UIImage(information: photoData)

PhotoData is an easy NSObject that holds the full-size picture and its personal model quantity. You implement the NSCoding protocol to encode and decode these to an information buffer.

Subsequent, open PhotoMetadata.swift and paste this after the imports:

class PhotoMetadata: NSObject, NSCoding

PhotoMetadata does the identical as PhotoData, besides the picture it shops can be a lot smaller. In a extra fully-featured app, you can be storing different details about the picture in right here (like notes or scores), which is why it’s a separate kind.

Congrats, you now have the mannequin courses for PhotoKeeper!

Subclassing UIDocument

UIDocument is an summary base class. This implies you could subclass it and implement sure required strategies earlier than it may be used. Particularly, it’s important to override two strategies:

load(fromContents:ofType:) That is the place you learn the doc and decode the mannequin information.

contents(forType:) Use this to jot down the mannequin into the doc.

First, you’ll outline some extra constants. Open Doc.swift after which add this proper above the category definition for Doc:

non-public extension String

You’ll use these constants to encode and decode your UIDocument recordsdata.

Subsequent, add these properties to the Doc class:

// 1
override var description: String

// 2
var fileWrapper: FileWrapper?

// three
lazy var photoData: PhotoData =
// TODO: Implement initializer
return PhotoData()
()

lazy var metadata: PhotoMetadata =
// TODO: Implement initializer
return PhotoMetadata()
()

// four
var picture: PhotoEntry?
get

set

Right here’s what you probably did:

You override description to return the title of the doc by taking the fileURL, eradicating the “ptk” extension and grabbing the final a part of the trail part.

fileWrapper is the OS file system node representing the listing that comprises your picture and metadata.

photoData and photoMetadata are the info fashions used to interpret the picture.metadata and picture.information subfiles the fileWrapper comprises. These are lazy variables, and also you’ll be including code to tug them from recordsdata in a while.

picture is the property used to entry and replace your most important and thumbnail picture once you make adjustments. It’s aliased PhotoEntry kind merely comprises your two pictures.

Subsequent, it’s time so as to add the code to jot down the UIDocument to disk.

First, add these strategies under the properties you’ve simply added:

non-public func encodeToWrapper(object: NSCoding) -> FileWrapper
let archiver = NSKeyedArchiver(requiringSecureCoding: false)
archiver.encode(object, forKey: .dataKey)
archiver.finishEncoding()

return FileWrapper(regularFileWithContents: archiver.encodedData)

override func contents(forType typeName: String) throws -> Any

encodeToWrapper(object:) makes use of NSKeyedArchiver to transform the article that implements NSCoding into an information buffer. Then it creates a FileWrapper file with the buffer and provides it to the listing.

To put in writing information to your doc, you implement contents(forType:). You encode every mannequin kind right into a FileWrapper, then create a dictionary of wrappers with filenames as keys. Lastly, you employ this dictionary to create one other FileWrapper wrapping the listing.

Nice! Now you may implement studying. Add the next strategies:

override func load(fromContents contents: Any, ofType typeName: String?) throws
guard let contents = contents as? FileWrapper else

fileWrapper = contents

func decodeFromWrapper(for title: String) -> Any?
guard
let allWrappers = fileWrapper,
let wrapper = allWrappers.fileWrappers?[name],
let information = wrapper.regularFileContents
else
return nil

do
let unarchiver = attempt NSKeyedUnarchiver.init(forReadingFrom: information)
unarchiver.requiresSecureCoding = false
return unarchiver.decodeObject(forKey: .dataKey)
catch let error

You want load(fromContents:ofType:) to implement studying. All you do is initialize the fileWrapper with the contents.

decodeFromWrapper(for:) does the alternative of encodeToWrapper(object:). It reads the suitable FileWrapper file from the listing FileWrapper and converts the info contents again to an object by way of the NSCoding protocol.

The very last thing to do is implement the getters for photoData and photoMetadata.

First, change the lazy initializer for photoData with:

//1
guard
fileWrapper != nil,
let information = decodeFromWrapper(for: .dataFilename) as? PhotoData
else

return information

Then, change the lazy initializer for photoMetadata with:

guard
fileWrapper != nil,
let information = decodeFromWrapper(for: .metadataFilename) as? PhotoMetadata
else
return PhotoMetadata()

return information

Each lazy initializers do just about the identical factor, however they search for fileWrappers with totally different names. You attempt to decode the suitable file from the fileWrapper listing as an example of your information mannequin class.

Creating Paperwork

Earlier than you may show an inventory of paperwork, you want to have the ability to add at the very least one so that you’ve got one thing to have a look at. There are three issues it’s essential do to create a brand new doc on this app:

Retailer entries.
Discover an accessible URL.
Create the doc.

Storing Entries

If you happen to create an entry within the app, you’ll see the creation date within the cell. As an alternative of displaying dates, you wish to show details about your paperwork, just like the thumbnail or your individual textual content.

All of this info is held in one other class referred to as Entry. Every Entry is represented by a cell within the desk view.

First, open Entry.swift and change the category implementation — however not the Comparable extension! — with:

class Entry: NSObject
var fileURL: URL
var metadata: PhotoMetadata?
var model: NSFileVersion

non-public var editDate: Date
return model.modificationDate ?? .distantPast

override var description: String
return fileURL.deletingPathExtension().lastPathComponent

init(fileURL: URL, metadata: PhotoMetadata?, model: NSFileVersion)
self.fileURL = fileURL
self.metadata = metadata
self.model = model

Entry merely retains monitor of all of the objects mentioned above. Ensure you don’t delete the Comparable!

At this level, you’ll see a compiler error, so it’s important to clear the code up a bit.

Now, go to ViewController.swift and take away this piece of code. You’ll change it later:

non-public func addOrUpdateEntry()

Because you simply eliminated addOrUpdateEntry, you’ll see one other compiler error:

Delete the road calling addOrUpdateEntry() in addEntry(_:).

Discovering an Accessible URL

The subsequent step is to discover a URL the place you wish to create the doc. This isn’t as simple because it sounds as a result of it’s essential auto-generate a filename that isn’t already taken. First, you’ll examine if a file exists.

Go to ViewController.swift. On the high, you’ll see two properties:

non-public var selectedEntry: Entry?
non-public var entries: [Entry] = []

selectedEntry will provide help to maintain monitor of the entry the consumer is interacting with.

entries is an array that comprises all of the entries on the disk.

To examine if a file exists, look via entries to see if the title is already taken.

Now, add two extra properties under:

non-public lazy var localRoot: URL? = FileManager.default.urls(
for: .documentDirectory,
in: .userDomainMask).first
non-public var selectedDocument: Doc?

The localRoot occasion variable retains monitor of the doc’s listing. The selectedDocument can be used for passing information between the grasp and element view controllers.

Now, underneath viewDidLoad() add this methodology to return the total path for a file for a particular filename:

non-public func getDocumentURL(for filename: String) -> URL?
return localRoot?.appendingPathComponent(filename, isDirectory: false)

Then underneath that, add a way that checks whether or not the filename already exists:

non-public func docNameExists(for docName: String) -> Bool
return !entries.filter.isEmpty

If the filename does exist already, you wish to discover a new one.

So, add a way to discover a non-taken title:

non-public func getDocFilename(for prefix: String) -> String

getDocFilename(for:) begins with the doc title handed in and checks whether it is accessible. If not, it provides 1 to the tip of the title and tries once more till it finds an accessible title.

Making a Doc

There are two steps to create a Doc. First, initialize the Doc with the URL to avoid wasting the file to. Then, name saveToURL to avoid wasting the recordsdata.

After you create the doc, it’s essential replace the objects array to retailer the doc and show the element view controller.

Now add this code under indexOfEntry(for:) to search out the index of an entry for a particular fileURL:

non-public func indexOfEntry(for fileURL: URL) -> Int?

Subsequent, add a way so as to add or replace an entry under:

non-public func addOrUpdateEntry(
for fileURL: URL,
metadata: PhotoMetadata?,
model: NSFileVersion
)
if let index = indexOfEntry(for: fileURL)
let entry = entries[index]
entry.metadata = metadata
entry.model = model
else

entries = entries.sorted(by: >)
tableView.reloadData()

addOrUpdateEntry(for:metadata:model:) finds the index of an entry for a particular fileURL. If it exists, it updates its properties. If it doesn’t, it creates a brand new Entry.

Lastly, add a way to insert a brand new doc:

non-public func insertNewDocument(
with photoEntry: PhotoEntry? = nil,
title: String? = nil)

You’re lastly placing all of the helper strategies you wrote to good use. Right here, the code you added:

Finds an accessible file URL within the native listing.
Initializes a Doc.
Saves the doc instantly.
Provides an entry to the desk.

Now, add the next to addEntry(_:) to name your new code:

insertNewDocument()

Ultimate Modifications

You’re nearly prepared to check this out!

Discover tableView(_:cellForRowAt:) and change the cell configuration with:

cell.photoImageView?.picture = entry.metadata?.picture
cell.titleTextField?.textual content = entry.description
cell.subtitleLabel?.textual content = entry.model.modificationDate?.mediumString

Construct and run your venture. You need to now have the ability to faucet the + button to create new paperwork that get saved on the file system:

If you happen to have a look at the console output, you need to see messages exhibiting the total paths of the place you’re saving the paperwork, like this:

File created at: file:///Customers/leamaroltsonnenschein/Library/Developer/CoreSimulator/Gadgets/C1176DC2-9AF9-48AB-A488-A1AB76EEE8E7/information/Containers/Knowledge/Software/B9D5780E-28CA-4CE9-A823-0808F8091E02/Paperwork/Picture.PTK

Nonetheless, this app has an enormous drawback. If you happen to construct and run the app once more, nothing will present up within the listing!

That’s as a result of there isn’t a code to listing paperwork but. You’ll add that now.

Itemizing Native Paperwork

To listing native paperwork, you’re going to get the URLs for all of the paperwork within the native Paperwork listing and open each. You’ll learn the metadata out to get the thumbnail however not the info, so issues keep environment friendly. Then, you’ll shut it once more and add it to the desk view.

In ViewController.swift, it’s essential add the strategy that masses a doc given a file URL. Add this proper beneath the viewDidLoad():

non-public func loadDoc(at fileURL: URL) {
let doc = Doc(fileURL: fileURL)
doc.open [weak self] success in
guard success else

let metadata = doc.metadata
let fileURL = doc.fileURL
let model = NSFileVersion.currentVersionOfItem(at: fileURL)

doc.shut() success in
guard success else
fatalError(“Failed to shut doc.”)

if let model = model

}

Right here you open the doc, get the knowledge it’s essential create an Entry and show the thumbnails. You then shut it once more moderately than conserving it open. That is necessary for 2 causes:

It avoids the overhead of conserving a complete UIDocument in reminiscence once you solely want one half.

UIDocuments can solely be opened and closed as soon as. If you wish to open the identical fileURL once more, it’s important to create a brand new UIDocument occasion.

Add these strategies to carry out the refresh beneath the strategy you simply added:

non-public func loadLocal()
guard let root = localRoot else
do catch let error
fatalError(“Could not load native content material. (error.localizedDescription)”)

non-public func refresh()
loadLocal()
tableView.reloadData()

This code iterates via all of the recordsdata within the Paperwork listing and masses each doc along with your app’s file extension.

Now, it’s essential add the next to the underside of viewDidLoad() to load the doc listing when the app begins:

refresh()

Construct and run. Now your app ought to accurately choose up the listing of paperwork since final time you ran it.

Creating Precise Entries

It’s lastly time to create actual entries for PhotoKeeper. There are two instances to cowl for including images:

Including a brand new entry.
Modifying an outdated entry.

Each these instances will current the DetailViewController. Nonetheless, when the consumer desires to edit an entry, you’ll go that doc via from the selectedDocument property on ViewController to the doc property on DetailViewController.

Nonetheless in ViewController.swift, add a way that presents the element view controller under insertNewDocument(with:title:):

non-public func showDetailVC()

Right here you entry the computed property detailVC, if potential, and go the selectedDocument if it exists. If it’s nil, then you’re creating a brand new doc. mode = .viewing lets the view controller know that it’s in viewing moderately than modifying mode.

Now go to the UITableViewDelegate extension and implement tableView(_:didSelectRowAt):

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
let entry = entries[indexPath.row]
selectedEntry = entry
selectedDocument = Doc(fileURL: entry.fileURL)

showDetailVC()

tableView.deselectRow(at: indexPath, animated: false)

Right here, you seize the entry the consumer chosen, populate the selectedEntry and selectedDocument properties and present the element view controller.

Now change the addEntry(_:) implementation with:

selectedEntry = nil
selectedDocument = nil
showDetailVC()

Right here you empty selectedEntry and selectedDocument earlier than exhibiting the element view controller to point that you just wish to create a brand new doc.

Construct and run. Now attempt including a brand new entry.

Wanting good, however nothing occurs once you faucet on Accomplished. Time to repair that!

An entry consists of a title and two pictures. The consumer can kind in a title within the textual content area and choose a photograph by interacting with the UIImagePickerController after tapping the Add/Edit Picture button.

Go to DetailViewController.swift.

First, it’s essential implement openDocument(). It will get referred to as on the finish of viewDidLoad() to lastly open the doc and entry the full-sized picture. Add this code to openDocument():

if doc == nil
else
doc?.open() [weak self] _ in
self?.fullImageView.picture = self?.doc?.picture?.mainImage
self?.titleTextField.textual content = self?.doc?.description

After opening the doc, you assign the saved picture to your fullImageView and the doc’s description because the title.

Retailer and Crop

When the consumer selects their picture, UIImagePickerController returns the data in imagePickerController(_:didFinishPickingMediaWithInfo:).

At that time, you wish to assign the chosen picture to fullImageView, create a thumbnail and save the total and thumbnail pictures of their respective native variables, newImage and newThumbnailImage.

Exchange the code in imagePickerController(_:didFinishPickingMediaWithInfo:) with:

guard let picture = data[UIImagePickerController.InfoKey.originalImage]
as? UIImage else
return

let choices = PHImageRequestOptions()
choices.resizeMode = .actual
choices.isSynchronous = true

if let imageAsset = data[UIImagePickerController.InfoKey.phAsset] as? PHAsset

fullImageView.picture = picture
let mainSize = fullImageView.bounds.measurement
newImage = picture.imageByBestFit(for: mainSize)

picker.dismiss(animated: true, completion: nil)

After guaranteeing the consumer picked a picture, you employ the Images and AssetsLibrary frameworks to create a thumbnail. As an alternative of getting to determine what essentially the most related rectangle of the picture to crop to is your self, these two frameworks do it for you!

Actually, the thumbnails look precisely the identical as they do in your Images library:

Evaluate and Save

Lastly, it’s essential implement what occurs when the consumer faucets the Accomplished button.

So, replace donePressed(_:) with:

var photoEntry: PhotoEntry?

if let newImage = newImage, let newThumb = newThumbnailImage

// 1
let hasDifferentPhoto = !newImage.isSame(picture: doc?.picture?.mainImage)
let hasDifferentTitle = doc?.description != titleTextField.textual content
hasChanges = hasDifferentPhoto || hasDifferentTitle

// 2
guard let doc = doc, hasChanges else
delegate?.detailViewControllerDidFinish(
self,
with: photoEntry,
title: titleTextField.textual content
)
dismiss(animated: true, completion: nil)
return

// three
doc.picture = photoEntry
doc.save(to: doc.fileURL, for: .forOverwriting)

After ensuring that the suitable pictures exist you:

Test whether or not there have been adjustments to both the photographs or title by evaluating the brand new pictures to the doc’s.
If you happen to did not go an current doc, you relinquish management to the delegate, the grasp view controller.
If you happen to did go a doc, you first save and overwrite it after which let the delegate do its magic.

Insert or Replace

The final piece of the puzzle is to both insert or replace this new information on the grasp view controller.

Go to ViewController.swift and discover the DetailViewControllerDelegate extension and implement the empty delegate methodology detailViewControllerDidFinish(_:with:title:):

// 1
guard
let doc = viewController.doc,
let model = NSFileVersion.currentVersionOfItem(at: doc.fileURL)
else
if let docData = photoEntry
return

// 2
if let docData = photoEntry

addOrUpdateEntry(for: doc.fileURL, metadata: doc.metadata, model: model)

This is what you added:

If the element view controller would not have a doc, you insert a brand new one.
If the doc exists, you merely replace the outdated entry.

Now, construct and run to see this in motion:

Success! You possibly can lastly create correct entries and even edit the images! However in case you attempt to change the title or delete an entry, the adjustments will solely be momentary and are available again with a vengeance once you give up and open the app.

That will not do.

Deleting and Renaming

For each deleting and renaming paperwork, you may use the FileManager, which provides you entry to a shared file supervisor object that permits you to interface with the contents of the file system and make adjustments to it.

First, return to ViewController.swift and alter the implementation of delete(entry:) to:

let fileURL = entry.fileURL
guard let entryIndex = indexOfEntry(for: fileURL) else

do catch

For deleting, you employ FileManager’s removeItem(at:) methodology. Whenever you construct and run, you may see now you can swipe rows to completely delete them. Make sure you shut down and restart the app to confirm they’re gone for good.

Subsequent, you may add the flexibility to rename paperwork.

First, add the next code to rename(_:with:):

guard entry.description != title else

let newDocFilename = “(title).(String.appExtension)”

if docNameExists(for: newDocFilename)
fatalError(“Title already taken.”)

guard let newDocURL = getDocumentURL(for: newDocFilename) else

do
attempt FileManager.default.moveItem(at: entry.fileURL, to: newDocURL)
catch
fatalError(“Could not transfer to new URL.”)

entry.fileURL = newDocURL
entry.model = NSFileVersion.currentVersionOfItem(at: entry.fileURL) ?? entry.model

tableView.reloadData()

For renaming, you employ FileManager’s moveItem(at:to:) methodology. The whole lot else within the methodology above is your run-of-the-mill desk view administration. Fairly easy, eh?

The very last thing to do is to examine whether or not the consumer has modified the title of the doc in detailViewControllerDidFinish(_:with:title:).

Return to that delegate methodology and add this code on the finish:

if let title = title, let entry = selectedEntry, title != entry.description
rename(entry, with: title)

Lastly, construct and run to check out this superior new solution to retailer images!

The place to Go From Right here?

Now that you’ve got gone via all of the steps of making a UIDocument from scratch, I like to recommend you check out our tutorial on utilizing UIDocumentBrowserViewController which makes it considerably simpler to deal with paperwork.

If you happen to’re taken with diving deeper into creating your individual paperwork and managing recordsdata, take a look at Apple’s documentation on UIDcocument and FileManager

If you happen to’re after a deep dive into NSCoding, check out this tutorial.

Lastly, to study extra about managing information in your apps, take a look at our video course on Saving Knowledge in iOS.

If in case you have any questions or feedback, please be a part of the discussion board dialogue under.

asubhan
wordpress autoblog
amazon autoblog
affiliate autoblog
wordpress web site
web site improvement

Related posts

Getting Began with Server-Facet Swift with Vapor [FREE]

admin

Catalyst by Tutorials [SUBSCRIBER]

admin

Superior iOS Mega Bundle – 2019 [SUBSCRIBER]

admin

Leave a Comment