Retired Document
Important: This document may not represent best practices for current development. Links to downloads and other resources may no longer be valid.
CloudPhotos (OS X).swift/CloudPhotos/DetailViewController.swift
/* |
Copyright (C) 2017 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
This sample's detail view controller showing the attributes of a specific CKRecords. |
*/ |
import Cocoa |
import MediaLibrary |
class DetailViewController : NSViewController, PhotoViewDelegate { |
// MARK: - Constants |
private enum TabIndex: Int { |
case tabOne = 0 |
case tabTwo = 1 |
} |
struct SegueIdentifier { |
static let mapPhoto = "mapPhoto" |
static let showPhoto = "showPhoto" |
static let editPhoto = "editPhoto" |
} |
var detailViewDelegate: DetailViewControllerDelegate! = nil |
// MARK: - Properties |
@IBOutlet private weak var photoImageView: PhotoView! |
@IBOutlet private weak var photoNameField: NSTextField! |
@IBOutlet private weak var photoOwnerField: NSTextField! |
@IBOutlet private weak var photoDateField: NSTextField! |
@IBOutlet private weak var photoLocationField: NSTextField! |
@IBOutlet private weak var mapButton: NSButton! |
@IBOutlet private weak var openPhotoButton: NSButton! |
@IBOutlet private weak var ownerProgressIndicator: NSProgressIndicator! |
@IBOutlet private weak var tabView : NSTabView! |
// Listen for updates due to push notification processing, so we can update our UI. |
private var notifObserver : NSObjectProtocol! |
// MARK: - Value Transformers |
// Transformer to converts MLMediaObject to NSImage (used by Interface Builder). |
let imageTransformer: MediaObjectToImageTransformer = { |
let imageTransformer = MediaObjectToImageTransformer() |
// Add to the name-based registry for shared objects used when |
// loading nib files with transformers specified by name in Interface Builder. |
// |
ValueTransformer.setValueTransformer(imageTransformer, forName: NSValueTransformerName("MediaObjectToImageTransformer")) |
return imageTransformer |
}() |
// Transformer to converts MLMediaObject to Title (used by Interface Builder). |
let titleTransformer: MediaObjectToTitleTransformer = { |
let titleTransformer = MediaObjectToTitleTransformer() |
// Add to the name-based registry for shared objects used when |
// loading nib files with transformers specified by name in Interface Builder. |
// |
ValueTransformer.setValueTransformer(titleTransformer, forName: NSValueTransformerName("MediaObjectToTitleTransformer")) |
return titleTransformer |
}() |
// transformer to converts MLMediaObject to Location (used by Interface Builder). |
let locationTransformer: MediaObjectToLocationTransformer = { |
let locationTransformer = MediaObjectToLocationTransformer() |
// Add to the name-based registry for shared objects used when |
// loading nib files with transformers specified by name in Interface Builder. |
// |
ValueTransformer.setValueTransformer(locationTransformer, forName: NSValueTransformerName("MediaObjectToLocationTransformer")) |
return locationTransformer |
}() |
// transformer to converts MLMediaObject to Location (used by Interface Builder). |
let dateTransformer: MediaObjectToModDateTransformer = { |
let dateTransformer = MediaObjectToModDateTransformer() |
// Add to the name-based registry for shared objects used when |
// loading nib files with transformers specified by name in Interface Builder. |
// |
ValueTransformer.setValueTransformer(dateTransformer, forName: NSValueTransformerName("MediaObjectToModDateTransformer")) |
return dateTransformer |
}() |
var detailItemRecord: CloudPhoto! { |
didSet { |
if detailItemRecord == nil { |
// Switch to tab 0 for the no selected photo UI. |
// |
tabView.selectTabViewItem(at: TabIndex.tabOne.rawValue) |
} |
else { |
// Switch to tab 1 for the detailed photo UI. |
// |
tabView.selectTabViewItem(at: TabIndex.tabTwo.rawValue) |
// name: |
photoNameField.stringValue = detailItemRecord.photoTitle |
photoNameField.isEnabled = detailItemRecord.isMyPhoto |
// owner: |
// Provide the owner of the current photo. |
photoOwnerField.stringValue = "" // Clear the owner field before starting. |
// Before obtaining the photo owner's name, check if we have our cloud service. |
CloudManager.cloudServiceAvailable { [weak self] (available) -> Void in |
if available { |
// Start progress indicator in its place in case photoOwner() takes some time. |
self?.ownerProgressIndicator.isHidden = false |
self?.ownerProgressIndicator.startAnimation(self) |
// Ask for the familyName of the owner of this photo. |
self?.detailItemRecord.photoOwner() { [weak self] (owner) in // owner return the familyName |
self?.ownerProgressIndicator.isHidden = true |
self?.ownerProgressIndicator.stopAnimation(self) |
if let userName = owner { |
self?.photoOwnerField.stringValue = userName |
self?.photoOwnerField.textColor = NSColor.labelColor |
} |
else { |
self?.setUnknownOwner() |
} |
} |
} |
else { |
self?.setUnknownOwner() |
// No editing this photo while the network is unavailable. |
self?.photoNameField.isEnabled = false |
} |
} |
// photo date: |
if let photoDate = detailItemRecord.photoDate { |
photoDateField.stringValue = dateFormatter.string(from: photoDate as Date) |
} |
// photo: |
photoImageView.image = detailItemRecord.photoImage |
openPhotoButton.isEnabled = detailItemRecord.photoImage != nil |
// Set address to unavailable for now until we know of a valid one. |
photoLocationField.stringValue = NSLocalizedString("Location Unavailable", comment: "") |
photoLocationField.textColor = NSColor.gray |
guard let location = detailItemRecord.photoLocation else { |
mapButton.isEnabled = false |
return |
} |
// location: |
// The map button enables/disables according to a valid CLLocation. |
mapButton.isEnabled = true |
// Get nearby readable address. |
geocoder.reverseGeocodeLocation(location, completionHandler: { [weak self] (placemarks, error) -> Void in |
if placemarks != nil && placemarks!.count > 0 { |
let placemark = placemarks![0] |
var locationString = "" |
if placemark.thoroughfare != nil { |
locationString = String(format:"%@ ", placemark.thoroughfare!) |
} |
if placemark.locality != nil { |
locationString = locationString + String(format:"%@ ", placemark.locality!) |
} |
if placemark.administrativeArea != nil { |
locationString = locationString + String(format:"%@ ", placemark.administrativeArea!) |
} |
if locationString.characters.count > 0 { |
self?.photoLocationField.stringValue = locationString |
self?.photoLocationField.textColor = NSColor.labelColor |
} |
} |
}) |
} |
} |
} |
private let dateFormatter: DateFormatter = { |
let dateFormatter: DateFormatter = DateFormatter() |
dateFormatter.dateStyle = .medium |
return dateFormatter |
}() |
private let geocoder: CLGeocoder = { |
let geocoder = CLGeocoder() |
return geocoder |
}() |
// MARK: - View Controller Lifecycle |
override func viewDidLoad() { |
super.viewDidLoad() |
let updateContentNotifName : NSNotification.Name = NSNotification.Name(APLCloudManager.updateContentWithNotification()) |
// Listen for updates due to push notification processing, so we can update our UI. |
notifObserver = NotificationCenter.default.addObserver( |
forName: updateContentNotifName, |
object: nil, |
queue: OperationQueue.main) { notification in |
// A push notification (CKQueryNotification) has arrived, |
// update our table for added, removed or updates photos. |
// |
let queryNotification = notification.object as! CKQueryNotification |
let reason = queryNotification.queryNotificationReason |
let recordID = queryNotification.recordID! as CKRecordID |
// Notify the splitview's master and detail view controllers of this notification. |
self.handlePush(recordID, reason:reason) |
} |
photoImageView.delegate = self |
} |
deinit { |
NotificationCenter.default.removeObserver(notifObserver) |
} |
/// Changes the owner field to unknown state in disabled gray. |
private func setUnknownOwner() { |
photoOwnerField.stringValue = NSLocalizedString("Unknown User Name", comment: "") |
photoOwnerField.textColor = NSColor.gray |
} |
/// Utility method to display modification Error. |
private func showModifiedError(messageStr: String) { |
let alert = NSAlert() |
alert.addButton(withTitle: NSLocalizedString("OK Button Title", comment: "")) |
alert.messageText = NSLocalizedString("Unable to change photo message", comment: "") |
alert.informativeText = messageStr |
alert.alertStyle = .informational |
alert.beginSheetModal(for: view.window!, completionHandler: nil) |
} |
// MARK: - Push Notifications |
/** |
Called by our AppDelegate to handle a specific push notification of a specifc CKRecordID, |
that photo could have beed added, deleted or updated. This is done silently. |
*/ |
func handlePush(_ recordID: CKRecordID, reason: CKQueryNotificationReason) { |
if detailItemRecord != nil { |
if recordID.isEqual(detailItemRecord.cloudRecord.recordID) { // Is it our photo that's being changed? |
switch (reason) { |
case .recordUpdated: |
// Our current photo has come in that was updated. |
// |
if view.window!.sheets.count == 0 { |
let alert = NSAlert() |
alert.addButton(withTitle: NSLocalizedString("OK Button Title", comment: "")) |
let strFormat = NSLocalizedString("Photo Generic Changed Notif Message", comment: "") |
let deletedRecordTitle = detailItemRecord.photoTitle |
alert.messageText = String(format: strFormat, deletedRecordTitle) |
alert.alertStyle = .informational |
alert.beginSheetModal(for: view.window!, completionHandler: { (result) -> Void in |
CloudManager.fetchRecord(with: recordID) { [weak self] (foundRecord, error) in |
guard let foundRecord = foundRecord else { return } |
// Create the CloudPhoto object with the associated CKRecord. |
self?.detailItemRecord = CloudPhoto(record: foundRecord) |
} |
}) |
} |
break |
default: break |
} |
} |
} |
} |
override func prepare(for segue: NSStoryboardSegue, sender: Any?) { |
if segue.identifier == DetailViewController.SegueIdentifier.mapPhoto { |
let mapVC = segue.destinationController as! MapViewController |
mapVC.mapLocation = detailItemRecord.photoLocation |
} |
else if segue.identifier == DetailViewController.SegueIdentifier.showPhoto { |
// Hand off our image to the image view controller popover. |
let imageVC = segue.destinationController as! ImageViewController |
imageVC.image = photoImageView.image |
} |
else if segue.identifier == DetailViewController.SegueIdentifier.editPhoto { |
// We are about to modify an existing photo (which opens ChoosePhotoViewController). |
} |
} |
// MARK: - Actions |
@IBAction func doneEditingName(_ sender: NSTextField) { |
// Make sure we have a valid photo. |
guard let photoRecord = detailItemRecord else { return } |
let photoName = photoRecord.photoTitle |
if photoNameField.stringValue != photoName { |
// Name field does not match, which means the user changed it, so we save this change. |
// Save the photo with the new name. |
detailItemRecord.photoTitle = photoNameField.stringValue |
CloudManager.modifyRecord(detailItemRecord.record(), completionHandler: { [weak self] (modifiedRecord, error) -> Void in |
if error != nil { |
self?.showModifiedError(messageStr: (error?.localizedDescription)!) |
// Revert the photo name field back to its original value due to the error. |
self?.photoNameField.stringValue = photoName |
} |
else { |
// Create the CloudPhoto object with the associated CKRecord. |
self?.detailItemRecord = CloudPhoto(record: modifiedRecord!) |
// Inform our delegate (master table view) to change the actual title in its table to the new title. |
if let delegate = self?.detailViewDelegate { |
delegate.didChangeCloudRecord(self!, photoRecord: (self?.detailItemRecord)!) |
} |
} |
}) |
} |
} |
// MARK: - PhotoViewDelegate |
func isPhotoChangeable(_ photoView: PhotoView) -> Bool { |
return detailItemRecord.isMyPhoto |
} |
// Delegate responsible for updating this detail view with the newly received media object. |
func didReceiveMediaObject(_ photoView: PhotoView, mediaObject: MLMediaObject?) { |
// If the incoming media object is nil, the user performed a delete operation on this photo. |
if (mediaObject == nil) |
{ |
let alert = NSAlert() |
alert.addButton(withTitle: NSLocalizedString("OK Button Title", comment: "")) |
alert.addButton(withTitle: NSLocalizedString("Cancel Button Title", comment: "")) |
alert.messageText = NSLocalizedString("Confirm Delete Image", comment:"") |
alert.informativeText = NSLocalizedString("Confirm Delete Image Detail", comment: "") |
alert.alertStyle = .warning |
alert.beginSheetModal(for: view.window!, completionHandler: { (result) -> Void in |
if result == NSAlertFirstButtonReturn { |
// User wants to remove this photo (the user deleted the photo image itself). |
self.detailItemRecord.photoDate = nil |
self.detailItemRecord.photoLocation = nil |
self.detailItemRecord.photoImage = nil |
self.detailItemRecord.photoTitle = NSLocalizedString("Untitled Title", comment: "") |
CloudManager.modifyRecord(self.detailItemRecord.record(), completionHandler: { [weak self] (modifiedRecord, error) -> Void in |
if error != nil { |
self?.showModifiedError(messageStr: (error?.localizedDescription)!) |
} |
else { |
// Create the new CloudPhoto object with the associated CKRecord. |
self?.detailItemRecord = CloudPhoto(record: modifiedRecord!) |
// Inform our delegate to change the actual title in its table to the new title. |
if let delegate = self?.detailViewDelegate { |
delegate.didChangeCloudRecord(self!, photoRecord: self!.detailItemRecord) |
} |
} |
}) |
} |
}) |
} |
else { |
if let image = imageTransformer.transformedValue(mediaObject) { |
if let date = dateTransformer.transformedValue(mediaObject) { |
let location = locationTransformer.transformedValue(mediaObject) |
if let title = titleTransformer.transformedValue(mediaObject) { |
// Only set the title directly if we have a valid one. |
if ((title as? String)!.characters.count > 0) { |
detailItemRecord.photoTitle = (title as? String)! |
} |
detailItemRecord.photoDate = date as? Date |
detailItemRecord.photoLocation = location as? CLLocation |
// Update the photo, this will create a sized down/compressed cached image in the caches folder. |
let imageURL = CloudManager.createCachedImage(from: image as! NSImage) |
if imageURL != nil { |
detailItemRecord.setPhotoImageURL(imageURL!) |
CloudManager.modifyRecord(detailItemRecord.record(), completionHandler: { [weak self] (modifiedRecord, error) -> Void in |
if error != nil { |
self?.showModifiedError(messageStr: (error?.localizedDescription)!) |
} |
else { |
// Create the new CloudPhoto object with the associated CKRecord. |
self?.detailItemRecord = CloudPhoto(record: modifiedRecord!) |
// Inform our delegate to change the actual title in its table to the new title. |
if let delegate = self?.detailViewDelegate { |
delegate.didChangeCloudRecord(self!, photoRecord: self!.detailItemRecord) |
} |
} |
}) |
} |
} |
} |
} |
} |
} |
// Delegate responsible for updating this detail view with the newly received image. |
func didReceiveImage(_ photoView: PhotoView, image: NSImage?) { |
// Raw images do not have a specific date, so use today's date/time |
detailItemRecord.photoDate = Date() |
// Raw images do not have a specific location. |
detailItemRecord.photoLocation = nil |
// Update the photo, this will create a sized down/compressed cached image in the caches folder. |
let imageURL = CloudManager.createCachedImage(from: image) |
if imageURL != nil { |
detailItemRecord.setPhotoImageURL(imageURL!) |
} else { |
detailItemRecord.photoImage = nil |
} |
// Update the photo record. |
CloudManager.modifyRecord(detailItemRecord.record(), completionHandler: { [weak self] (modifiedRecord, error) -> Void in |
if error != nil { |
self?.showModifiedError(messageStr: (error?.localizedDescription)!) |
} |
else { |
// Create the new CloudPhoto object with the associated CKRecord. |
self?.detailItemRecord = CloudPhoto(record: modifiedRecord!) |
// Inform our delegate to change the actual title in its table to the new title. |
if let delegate = self?.detailViewDelegate { |
delegate.didChangeCloudRecord(self!, photoRecord: self!.detailItemRecord) |
} |
} |
}) |
} |
} |
// MARK: - DetailViewControllerDelegate |
protocol DetailViewControllerDelegate { |
func didChangeCloudRecord(_ detailViewController: DetailViewController, photoRecord: CloudPhoto) |
} |
Copyright © 2017 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2017-03-09