Related
Had this working on the last few versions of iOS but on 14 cannot get this to work, I have spent the past 5 days Googling.
Whenever I go to add an image from the gallery the popup to select an image is blank.
I have tried UIImagePickerController(which works fine iOS13 and below) and the new PHPickerViewController.
What has been working up until iOS14 is the user clicks a bar button to "add image" and the pop up asks to pick from the gallery and then presents the picker with images in your gallery.
I have set the Info.plist "Privacy - Photo Library Usage Description" for UIImagePickerController, I know the PHPickerViewController doesn't need perms as it handles getting the image for you.
I have also been seeing these purple warning now and again saying "xxxx must be used from main thread only", never seen that before until the new Xcode.
Any help would be much appreciated. Running Xcode 12.0.1 (12A7300), simulators running iOS14.
Adam
class AddCustomExerciseViewController: UIViewController, UITextViewDelegate, UINavigationControllerDelegate, GADBannerViewDelegate, UIImagePickerControllerDelegate, PHPickerViewControllerDelegate{
let alert = UIAlertController(title: "Choose Image", message: nil, preferredStyle: .actionSheet)
alert.addAction(UIAlertAction(title: "Gallery", style: .default, handler: { _ in self.openGallery() }))
alert.addAction(UIAlertAction.init(title: "Cancel", style: .cancel, handler: nil))
self.present(alert, animated: true, completion: nil)
func openGallery()
{
let photoAuthorizationStatus = PHPhotoLibrary.authorizationStatus()
switch photoAuthorizationStatus {
case .authorized:
self.showGallery()
print("Access is granted by user")
case .notDetermined:
PHPhotoLibrary.requestAuthorization({
(newStatus) in
print("status is \(newStatus)")
if newStatus == PHAuthorizationStatus.authorized {
self.showGallery()
print("success")
}
})
print("It is not determined until now")
case .restricted:
// same same
print("User do not have access to photo album.")
case .denied:
// same same
print("User has denied the permission.")
case .limited:
// same same
print("User has denied the permission.")
}
}
func showGallery()
{
if #available(iOS 14, *)
{
var configuration = PHPickerConfiguration()
configuration.filter = .images
configuration.selectionLimit = 0
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = self
self.present(picker, animated: true, completion: nil)
}
else
{
if UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.photoLibrary){
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.allowsEditing = false
imagePicker.sourceType = UIImagePickerController.SourceType.photoLibrary
self.present(imagePicker, animated: true, completion: nil)
}
else
{
let alert = UIAlertController(title: "Warning", message: "You don't have permission to access gallery.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
}
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
if let pickedImage = info[UIImagePickerControllerOriginalImage] as? UIImage {
//get the file url
if let fileUrl = info[UIImagePickerControllerImageURL] as? URL {
print(fileUrl)
print(fileUrl.lastPathComponent)
let filePath = fileUrl.lastPathComponent
if(filePath.hasSuffix("GIF")||filePath.hasSuffix("gif"))
{
//to change later to support gifs
gifView.image = pickedImage
}
else
{
//to show the static image to users
gifView.image = pickedImage
}
//for saving the file name to retrieve in local parse datastore
imageNameToSave = fileUrl.lastPathComponent
}
//for saving the selected image
imageToSave = pickedImage
}
picker.dismiss(animated: true, completion: nil)
}
#available(iOS 14, *)
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult])
{
picker.dismiss(animated: true, completion: nil)
for result in results
{
result.itemProvider.loadObject(ofClass: UIImage.self, completionHandler: { (object, error) in
if let image = object as? UIImage {
DispatchQueue.main.async {
// Use UIImage
print("Selected image: \(image)")
}
}
})
}
} }
On iOS 14 the restricted image picker permission dialog opens in a separate thread, so when the authorization return you will need to perform UI tasks on main thread.
PHPhotoLibrary.requestAuthorization({
(newStatus) in
DispatchQueue.main.async {
print("status is \(newStatus)")
if newStatus == PHAuthorizationStatus.authorized {
self.showGallery()
print("success")
}
}
})
If this is helpful for anyone else I discovered that editing the scrollview appearance proxy background caused a bug in both the UIImagePickerController and the PHPickerViewController preventing them from showing images in iOS 14 and above. I had edited the scrollview background proxy because it is black even in light mode. After hours of googling and testing I discovered that removing the appearance background modification on the scrollview proxy allowed the image pickers to function as expected. I filed a bug report with Apple. The line of code that caused the bug was:
UIScrollView.appearance().backgroundColor = .systemBackground
I am trying to read images from Firebase storage, and I am getting this error:
NSError * domain: #"com.google.HTTPStatus" - code: 404
In function:
- (void)invokeFetchCallbacksOnCallbackQueueWithData:(GTM_NULLABLE NSData *)data
error:(GTM_NULLABLE NSError *)error {
// Callbacks will be released in the method stopFetchReleasingCallbacks:
GTMSessionFetcherCompletionHandler handler;
#synchronized(self) {
GTMSessionMonitorSynchronized(self);
handler = _completionHandler;
if (handler) {
[self invokeOnCallbackQueueUnlessStopped:^{
handler(data, error);
// Post a notification, primarily to allow code to collect responses for
// testing.
//
// The observing code is not likely on the fetcher's callback
// queue, so this posts explicitly to the main queue.
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
if (data) {
userInfo[kGTMSessionFetcherCompletionDataKey] = data;
}
if (error) {
userInfo[kGTMSessionFetcherCompletionErrorKey] = error;
}
[self postNotificationOnMainThreadWithName:kGTMSessionFetcherCompletionInvokedNotification
userInfo:userInfo
requireAsync:NO];
}];
}
} // #synchronized(self)
This is my code for reading from Firebase Storage:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "product_collection_cell", for: indexPath) as! ProductsCollectionViewCell
let prodInCell = searchActive ? filtered[indexPath.row] : products[indexPath.row]
// Set fields
cell.ProductImageView.image = #imageLiteral(resourceName: "DefaultProductImage")
prodInCell.GetProductImage() { image in
cell.ProductImageView.image = image
}
cell.ProductName.text = prodInCell.Name()
cell.ProductPrice.text = String(prodInCell.Price())
cell.productUniqueID = prodInCell.UniqueID()
return cell
}
public func GetProductImage(completion: ((UIImage?) -> Void)) {
let prodID = self.UniqueID()
let dbRef = Storage.storage().reference().child(prodID).child("pic0.jpg")
let imgTask = dbRef.getData(maxSize: 10*1024*1024, completion: { (data, error) in
if let data = data, let img = UIImage(data: data) {
completion(img)
} else {
completion(nil)
}
})
imgTask.observe(.progress, handler: {(snapshot) in
print (snapshot.progress ?? "NO MORE PROGRESS")
})
imgTask.resume()
}
And I get exception: Thread 1: EXC_BAD_ACCESS (code=1, address=0x10)
What is the problem and how can I resolve this ?
Not all my products have images, and if one does not I want to display "DefaultProductImage".
This is an example of one of my products' images stored:
I got some error when I send photo message.
I'm a beginner with swift language. I want to create undergraduate project about chat app. I need some help to fix this problem. Everything had worked smoothly till now but I am facing a small bug that is when I receive an image then it don't appears on the left side of the screen both incoming and outgoing images appear on the right side of the screen.
import UIKit
import JSQMessagesViewController
import MobileCoreServices
import AVKit
import FirebaseDatabase
import FirebaseStorage
import FirebaseAuth
import SDWebImage
class ChatViewController: JSQMessagesViewController {
var messages = [JSQMessage]()
var avatarDict = [String: JSQMessagesAvatarImage]()
var messageRef = FIRDatabase.database().reference().child("messages")
override func viewDidLoad() {
super.viewDidLoad()
if let currentUser = FIRAuth.auth()?.currentUser
{
self.senderId = currentUser.uid
if currentUser.isAnonymous == true
{
self.senderDisplayName = "anonymous"
} else
{
self.senderDisplayName = "\(currentUser.displayName!)"
}
}
observeMessages()
}
func observeUsers(_ id: String)
{
FIRDatabase.database().reference().child("users").child(id).observe(.value, with: {
snapshot in
if let dict = snapshot.value as? [String: AnyObject]
{
let avatarUrl = dict["profileUrl"] as! String
self.setupAvatar(avatarUrl, messageId: id)
}
})
}
func setupAvatar(_ url: String, messageId: String)
{
if url != "" {
let fileUrl = URL(string: url)
let data = try? Data(contentsOf: fileUrl!)
let image = UIImage(data: data!)
let userImg = JSQMessagesAvatarImageFactory.avatarImage(with: image, diameter: 30)
self.avatarDict[messageId] = userImg
self.collectionView.reloadData()
} else {
avatarDict[messageId] = JSQMessagesAvatarImageFactory.avatarImage(with: UIImage(named: "profileImage"), diameter: 30)
collectionView.reloadData()
}
}
func observeMessages() {
messageRef.observe(.childAdded, with: { snapshot in
// print(snapshot.value)
if let dict = snapshot.value as? [String: AnyObject] {
let mediaType = dict["MediaType"] as! String
let senderId = dict["senderId"] as! String
let senderName = dict["senderName"] as! String
self.observeUsers(senderId)
switch mediaType {
case "TEXT":
let text = dict["text"] as! String
self.messages.append(JSQMessage(senderId: senderId, displayName: senderName, text: text))
case "PHOTO":
let photo = JSQPhotoMediaItem(image: nil)
let fileUrl = dict["fileUrl"] as! String
let downloader = SDWebImageDownloader.shared()
downloader.downloadImage(with: URL(string: fileUrl)!, options: [], progress: nil, completed: { (image, data, error, finished) in
DispatchQueue.main.async(execute: {
photo?.image = image
self.collectionView.reloadData()
})
})
self.messages.append(JSQMessage(senderId: senderId, displayName: senderName, media: photo))
if self.senderId == senderId {
photo?.appliesMediaViewMaskAsOutgoing = true
} else {
photo?.appliesMediaViewMaskAsOutgoing = false
}
case "VIDEO":
let fileUrl = dict["fileUrl"] as! String
let video = URL(string: fileUrl)!
let videoItem = JSQVideoMediaItem(fileURL: video, isReadyToPlay: true)
self.messages.append(JSQMessage(senderId: senderId, displayName: senderName, media: videoItem))
if self.senderId == senderId {
videoItem?.appliesMediaViewMaskAsOutgoing = true
} else {
videoItem?.appliesMediaViewMaskAsOutgoing = false
}
default:
print("unknown data type")
}
self.collectionView.reloadData()
}
})
}
override func didPressSend(_ button: UIButton!, withMessageText text: String!, senderId: String!, senderDisplayName: String!, date: Date!) {
let newMessage = messageRef.childByAutoId()
let messageData = ["text": text, "senderId": senderId, "senderName": senderDisplayName, "MediaType": "TEXT"]
newMessage.setValue(messageData)
self.finishSendingMessage()
}
override func didPressAccessoryButton(_ sender: UIButton!) {
print("didPressAccessoryButton")
let sheet = UIAlertController(title: "Media Messages", message: "Please select a media", preferredStyle: UIAlertControllerStyle.actionSheet)
let cancel = UIAlertAction(title: "Cancel", style: UIAlertActionStyle.cancel) { (alert:UIAlertAction) in
}
let photoLibrary = UIAlertAction(title: "Photo Library", style: UIAlertActionStyle.default) { (alert: UIAlertAction) in
self.getMediaFrom(kUTTypeImage)
}
let videoLibrary = UIAlertAction(title: "Video Library", style: UIAlertActionStyle.default) { (alert: UIAlertAction) in
self.getMediaFrom(kUTTypeMovie)
}
sheet.addAction(photoLibrary)
sheet.addAction(videoLibrary)
sheet.addAction(cancel)
self.present(sheet, animated: true, completion: nil)
}
func getMediaFrom(_ type: CFString) {
print(type)
let mediaPicker = UIImagePickerController()
mediaPicker.delegate = self
mediaPicker.mediaTypes = [type as String]
self.present(mediaPicker, animated: true, completion: nil)
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, messageDataForItemAt indexPath: IndexPath!) -> JSQMessageData! {
return messages[indexPath.item]
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, messageBubbleImageDataForItemAt indexPath: IndexPath!) -> JSQMessageBubbleImageDataSource! {
let message = messages[indexPath.item]
let bubbleFactory = JSQMessagesBubbleImageFactory()
if message.senderId == self.senderId {
return bubbleFactory!.outgoingMessagesBubbleImage(with: .black)
} else {
return bubbleFactory!.incomingMessagesBubbleImage(with: .blue)
}
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAt indexPath: IndexPath!) -> JSQMessageAvatarImageDataSource! {
let message = messages[indexPath.item]
return avatarDict[message.senderId]
//return JSQMessagesAvatarImageFactory.avatarImageWithImage(UIImage(named: "profileImage"), diameter: 30)
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
print("number of item:\(messages.count)")
return messages.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = super.collectionView(collectionView, cellForItemAt: indexPath) as! JSQMessagesCollectionViewCell
return cell
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, didTapMessageBubbleAt indexPath: IndexPath!) {
print("didTapMessageBubbleAtIndexPath: \(indexPath.item)")
let message = messages[indexPath.item]
if message.isMediaMessage {
if let mediaItem = message.media as? JSQVideoMediaItem {
let player = AVPlayer(url: mediaItem.fileURL)
let playerViewController = AVPlayerViewController()
playerViewController.player = player
self.present(playerViewController, animated: true, completion: nil)
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func logoutDidTapped(_ sender: AnyObject) {
do {
try FIRAuth.auth()?.signOut()
} catch let error {
print(error)
}
// Create a main storyboard instance
let storyboard = UIStoryboard(name: "Main", bundle: nil)
// From main storyboard instantiate a View controller
let LogInVC = storyboard.instantiateViewController(withIdentifier: "LogInVC") as! LogInViewController
// Get the app delegate
let appDelegate = UIApplication.shared.delegate as! AppDelegate
// Set LogIn View Controller as root view controller
appDelegate.window?.rootViewController = LogInVC
}
func sendMedia(_ picture: UIImage?, video: URL?) {
print(picture)
print(FIRStorage.storage().reference())
if let picture = picture {
let filePath = "\(FIRAuth.auth()!.currentUser)/\(Date.timeIntervalSinceReferenceDate)"
print(filePath)
let data = UIImageJPEGRepresentation(picture, 0.1)
let metadata = FIRStorageMetadata()
metadata.contentType = "image/jpg"
FIRStorage.storage().reference().child(filePath).put(data!, metadata: metadata) { (metadata, error)
in
if error != nil {
print(error?.localizedDescription)
return
}
let fileUrl = metadata!.downloadURLs![0].absoluteString
let newMessage = self.messageRef.childByAutoId()
let messageData = ["fileUrl": fileUrl, "senderId": self.senderId, "senderName": self.senderDisplayName, "MediaType": "PHOTO"]
newMessage.setValue(messageData)
}
} else if let video = video {
let filePath = "\(FIRAuth.auth()!.currentUser)/\(Date.timeIntervalSinceReferenceDate)"
print(filePath)
let data = try? Data(contentsOf: video)
let metadata = FIRStorageMetadata()
metadata.contentType = "video/mp4"
FIRStorage.storage().reference().child(filePath).put(data!, metadata: metadata) { (metadata, error)
in
if error != nil {
print(error?.localizedDescription)
return
}
let fileUrl = metadata!.downloadURLs![0].absoluteString
let newMessage = self.messageRef.childByAutoId()
let messageData = ["fileUrl": fileUrl, "senderId": self.senderId, "senderName": self.senderDisplayName, "MediaType": "VIDEO"]
newMessage.setValue(messageData)
}
}
}
}
extension ChatViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
print("did finish picking")
// get the image
print(info)
if let picture = info[UIImagePickerControllerOriginalImage] as? UIImage {
sendMedia(picture, video: nil)
}
else if let video = info[UIImagePickerControllerMediaURL] as? URL {
sendMedia(nil, video: video)
}
self.dismiss(animated: true, completion: nil)
collectionView.reloadData()
}
}
You just need to understand what is going on in your code , as i am looking into your code you are reloading collectionview before the JSQPhotoMediaItem or JSQVideoMediaItem objects are completely configured and added these MediaItem into your message array, so make sure you have completely configured your JSQPhotoMediaItem and JSQVideoMediaItem objects and only after that you add these object to your message array.
you can add this into your switch case condition
case "PHOTO":
let photo = JSQPhotoMediaItem(image: nil)
let fileUrl = dict["fileUrl"] as! String
let downloader = SDWebImageDownloader.shared()
downloader.downloadImage(with: URL(string: fileUrl)!, options: [], progress: nil, completed: { (image, data, error, finished) in
DispatchQueue.main.async(execute: {
photo?.image = image // you have image in your media object
if self.senderId == senderId {
photo?.appliesMediaViewMaskAsOutgoing = true
} else {
photo?.appliesMediaViewMaskAsOutgoing = false
}
// you just configured media object by using appliesMediaViewMaskAsOutgoing
self.messages.append(JSQMessage(senderId: senderId, displayName: senderName, media: photo))
// successfully added object into your message array now you should reload collectionview
self.collectionView.reloadData()
})
})
//same goes for your video condition
But still i would recommend you to follow each step one by one.
Follow these steps for adding A JSQPhotoMediaItem object
make a JSQPhotoMediaItem Object like
let photoItem = JSQPhotoMediaItem(image: UIImage(named: <your Image Object>))
make sure you have downloaded your image from server and successfully have your image object ready to add in the JSQPhotoMediaItem Object (as you are downloading it from firebase) first get the image then add it to your JSQPhotoMediaItem
now you have to tell your message type( outgoing or incoming ) so add appliesMediaViewMaskAsOutgoing property to according to you need so this would be in your code
if self.senderId == senderId {
photoItem?.appliesMediaViewMaskAsOutgoing = true
} else {
photoItem?.appliesMediaViewMaskAsOutgoing = false
}
now this is the final step for appending this object to your array
self.messages.append(JSQMessage(senderId: senderId, displayName: senderName, media: photoItem))
now reload collectionView as you have successfully configured your media item ( JSQPhotoMediaItem )
self.collectionView.reloadData()
same goes for the JSQVideoMediaItem objects.
I have a pretty complex data structure in my app, which I need to manipulate. I am trying to keep track of how many types of bugs a player has in thier garden. There are ten types of bugs, each with ten patterns, each pattern having ten colors. So there are 1000 unique bugs possible, and I want to track how many of each of these types the player has. The nested dictionary looks like:
var colorsDict: [String : Int]
var patternsDict: [String : Any] // [String : colorsDict]
var bugsDict: [String : Any] // [String : patternsDict]
I do not get any errors or complaints with this syntax.
When I want to increment the player's bug collection though, doing this:
bugs["ladybug"]["spotted"]["red"]++
I get this error: String is not convertible to 'DictionaryIndex< String, Any >' with the error's carrot under the first string.
Another similar post suggested using "as Any?" in the code, but the OP of that post only had a dictionary one deep so could do that easily with: dict["string"] as Any? ...
I am not sure how to do this with a multilevel dictionary. Any help would be appreciated.
When working with dictionaries you have to remember that a key might not exist in the dictionary. For this reason, dictionaries always return optionals. So each time you access the dictionary by key you have to unwrap at each level as follows:
bugsDict["ladybug"]!["spotted"]!["red"]!++
I presume you know about optionals, but just to be clear, use the exclamation mark if you are 100% sure the key exists in the dictionary, otherwise it's better to use the question mark:
bugsDict["ladybug"]?["spotted"]?["red"]?++
Addendum: This is the code I used for testing in playground:
var colorsDict = [String : Int]()
var patternsDict = [String : [String : Int]] ()
var bugsDict = [String : [String : [String : Int]]] ()
colorsDict["red"] = 1
patternsDict["spotted"] = colorsDict
bugsDict["ladybug"] = patternsDict
bugsDict["ladybug"]!["spotted"]!["red"]!++ // Prints 1
bugsDict["ladybug"]!["spotted"]!["red"]!++ // Prints 2
bugsDict["ladybug"]!["spotted"]!["red"]!++ // Prints 3
bugsDict["ladybug"]!["spotted"]!["red"]! // Prints 4
Another option: You could try calling dict.value( forKeyPath: "ladybug.spotted.red" )!
So I just tried this with Swift 5:
import Foundation
var d = [ "ladybug" : [ "spotted" : [ "red" : 123 ] ] ] as [String:Any]
(d as NSDictionary).value(forKeyPath: "ladybug.spotted.red")
and it works, but this is probably the best way:
d["ladybug"]?["spotted"]?["red"]
I had the same issue, where I wanted to get boolValue nested in dictionary.
{
"Level1": {
"leve2": {
"code": 0,
"boolValue": 1
}
}
}
I tried a lot of solution but those didn't worked for me as i was missing type casting. So I used following code to get the boolValue from json, where json is a nested dictionary of type [String:Any].
let boolValue = ((json["level1"]
as? [String: Any])?["level2"]
as? [String: Any])?["boolValue"] as? Bool
My primary use case was reading ad-hoc values from a deep dictionary. None of the answers given worked for me in my Swift 3.1 project, so I went looking and found Ole Begemann's excellent extension for Swift dictionaries, with a detailed explanation on how it works.
I've made a Github gist with the Swift file I made for using it, and I welcome feedback.
To use it, you can add the Keypath.swift into your project, and then you can simply use a keyPath subscript syntax on any [String:Any] dictionary as follows.
Considering you have a JSON object like so:
{
"name":"John",
"age":30,
"cars": {
"car1":"Ford",
"car2":"BMW",
"car3":"Fiat"
}
}
stored in a dictionary var dict:[String:Any]. You could use the following syntax to get to the various depths of the object.
if let name = data[keyPath:"name"] as? String{
// name has "John"
}
if let age = data[keyPath:"age"] as? Int{
// age has 30
}
if let car1 = data[keyPath:"cars.car1"] as? String{
// car1 has "Ford"
}
Note that the extension supports writing into nested dictionaries as well, but I haven't yet used this.
I still haven't found a way to access arrays within dictionary objects using this, but it's a start! I'm looking for a JSON Pointer implementation for Swift but haven't found one, yet.
If it's only about retrieval (not manipulation) then here's a Dictionary extension for Swift 3 (code ready for pasting into Xcode playground) :
//extension
extension Dictionary where Key: Hashable, Value: Any {
func getValue(forKeyPath components : Array<Any>) -> Any? {
var comps = components;
let key = comps.remove(at: 0)
if let k = key as? Key {
if(comps.count == 0) {
return self[k]
}
if let v = self[k] as? Dictionary<AnyHashable,Any> {
return v.getValue(forKeyPath : comps)
}
}
return nil
}
}
//read json
let json = "{\"a\":{\"b\":\"bla\"},\"val\":10}" //
if let parsed = try JSONSerialization.jsonObject(with: json.data(using: .utf8)!, options: JSONSerialization.ReadingOptions.mutableContainers) as? Dictionary<AnyHashable,Any>
{
parsed.getValue(forKeyPath: ["a","b"]) //-> "bla"
parsed.getValue(forKeyPath: ["val"]) //-> 10
}
//dictionary with different key types
let test : Dictionary<AnyHashable,Any> = ["a" : ["b" : ["c" : "bla"]], 0 : [ 1 : [ 2 : "bla"]], "four" : [ 5 : "bla"]]
test.getValue(forKeyPath: ["a","b","c"]) //-> "bla"
test.getValue(forKeyPath: ["a","b"]) //-> ["c": "bla"]
test.getValue(forKeyPath: [0,1,2]) //-> "bla"
test.getValue(forKeyPath: ["four",5]) //-> "bla"
test.getValue(forKeyPath: ["a","b","d"]) //-> nil
//dictionary with strings as keys
let test2 = ["one" : [ "two" : "three"]]
test2.getValue(forKeyPath: ["one","two"]) //-> "three"
Unfortunately none of these methods worked for me, so I built my own to use a simple string path like "element0.element1.element256.element1", etc. Hope this save a time for others. (just use a dots between name of elements in string)
Json example:
{
"control": {
"type": "Button",
"name": "Save",
"ui": {
"scale": 0.5,
"padding": {
"top": 24,
"bottom": 32
}
}
}
}
Step 1, convert json String to Dictionary
static func convertToDictionary(text: String) -> [String: Any]? {
if let data = text.data(using: .utf8) {
do {
return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
} catch {
print(error.localizedDescription)
}
}
return nil
}
Step 2, helper to get a nested objects
//path example: "control.ui.scale"
static func getDictValue(dict:[String: Any], path:String)->Any?{
let arr = path.components(separatedBy: ".")
if(arr.count == 1){
return dict[String(arr[0])]
}
else if (arr.count > 1){
let p = arr[1...arr.count-1].joined(separator: ".")
let d = dict[String(arr[0])] as? [String: Any]
if (d != nil){
return getDictValue(dict:d!, path:p)
}
}
return nil
}
Step 3, use helper
let controlScale = getDictValue(dict:dict, path: "control.ui.scale") as! Double?
print(controlScale)
let controlName = getDictValue(dict:dict, path: "control.name") as! String?
print(controlName)
Returns
0.5
Save
The Swift 4 default: subscript for Dictionaries makes makes updating values in nested Dictionaries much more concise.
Get and Set a default value rather than dealing with optionals:
var dict = [String : [String : String]]()
dict["deep", default: [:]]["nested"] = "dictionary"
print(dict)
// ["deep": ["nested": "dictionary"]]
https://swift.org/blog/dictionary-and-set-improvements/
You can use this extension:
extension Dictionary {
/// - Description
/// - The function will return a value on given keypath
/// - if Dictionary is ["team": ["name": "KNR"]] the to fetch team name pass keypath: team.name
/// - If you will pass "team" in keypath it will return team object
/// - Parameter keyPath: keys joined using '.' such as "key1.key2.key3"
func valueForKeyPath <T> (_ keyPath: String) -> T? {
let array = keyPath.components(separatedBy: ".")
return value(array, self) as? T
}
/// - Description:"
/// - The function will return a value on given keypath. It keep calling recursively until reach to the keypath. Here are few sample:
/// - if Dictionary is ["team": ["name": "KNR"]] the to fetch team name pass keypath: team.name
/// - If you will pass "team" in keypath it will return team object
/// - Parameters:
/// - keys: array of keys in a keypath
/// - dictionary: The dictionary in which value need to find
private func value(_ keys: [String], _ dictionary: Any?) -> Any? {
guard let dictionary = dictionary as? [String: Any], !keys.isEmpty else {
return nil
}
if keys.count == 1 {
return dictionary[keys[0]]
}
return value(Array(keys.suffix(keys.count - 1)), dictionary[keys[0]])
}
}
Usage:
let dictionary = ["values" : ["intValue": 3]]
let value: Int = dictionary.valueForKeyPath("values.intValue")
You can use the following syntax on Swift 3/4:
if let name = data["name"] as? String {
// name has "John"
}
if let age = data["age"] as? Int {
// age has 30
}
if let car = data["cars"] as? [String:AnyObject],
let car1 = car["car1"] as? String {
// car1 has "Ford"
}
Yet another approach using various overloaded Dictionary subscript implementations:
let dict = makeDictionary(fromJSONString:
"""
{
"control": {
"type": "Button",
"name": "Save",
"ui": {
"scale": 0.5,
"padding": {
"top": 24,
"bottom": 32
}
}
}
}
""")!
dict[Int.self, ["control", "ui", "padding", "top"]] // 1
dict[Int.self, "control", "ui", "padding", "top"] // 2
dict[Int.self, "control.ui.padding.top"] // 3
And the actual implementations:
extension Dictionary {
// 1
subscript<T>(_ type: T.Type, _ pathKeys: [Key]) -> T? {
precondition(pathKeys.count > 0)
if pathKeys.count == 1 {
return self[pathKeys[0]] as? T
}
// Drill down to the innermost dictionary accessible through next-to-last key
var dict: [Key: Value]? = self
for currentKey in pathKeys.dropLast() {
dict = dict?[currentKey] as? [Key: Value]
if dict == nil {
return nil
}
}
return dict?[pathKeys.last!] as? T
}
// 2. Calls 1
subscript<T>(_ type: T.Type, _ pathKeys: Key...) -> T? {
return self[type, pathKeys]
}
}
extension Dictionary where Key == String {
// 3. Calls 1
subscript<T>(_ type: T.Type, _ keyPath: String) -> T? {
return self[type, keyPath.components(separatedBy: ".")]
}
}
func makeDictionary(fromJSONString jsonString: String) -> [String: Any]? {
guard let data = jsonString.data(using: .utf8)
else { return nil}
let ret = try? JSONSerialization.jsonObject(with: data, options: [])
return ret as? [String: Any]
}
Yet another Dictionary extension
public extension Dictionary where Key: Hashable, Value: Any {
subscript(keyPath path: String) -> Value? {
self[keyPath: path.components(separatedBy: ".").compactMap { $0 as? Key }]
}
private subscript(keyPath keys: [Key]) -> Value? {
var keys = keys
switch keys.first {
case .some(let key) where keys.count == 1:
return self[key]
case .some(let key) where keys.count > 1:
keys.removeFirst()
return (self[key] as? Dictionary<Key, Value>)?[keyPath: keys]
default:
return nil
}
}
}
Test code:
let dict: [String: Any] = [
"user": [
"name": "Giorgio",
"surname": "Baldazzi"
]
]
let keyPath = "user.name"
print(String(describing: dict[keyPath: keyPath]))
I'm struggling with this Swift code already for some time and do not find the problem. The code
below should provide the File Directory as DataSource for a NSOutlineView. The GUI is quite simple
just a window with a NSOutlineView and a Object for the OutlineViewController instance.
When I start the application it shows the root entry, when I expand the root entry it shows for a short period the sub items. Then the application crashes with an Error in file "main.swift" at line "NSApplicationMain(C_ARGC, C_ARGV) --> "EXC_BAD_ACCESS(code=EXC_I386_GPFLT)" ?
If added some println() to proof the directory structure - this seems to be fine.
The swift code:
import Cocoa
import Foundation
class FileSystemItem {
let propertyKeys = [NSURLLocalizedNameKey, NSURLEffectiveIconKey, NSURLIsPackageKey, NSURLIsDirectoryKey,NSURLTypeIdentifierKey]
let fileURL: NSURL
var name: String! {
let resourceValues = fileURL.resourceValuesForKeys([NSURLNameKey], error: nil)
return resourceValues[NSURLNameKey] as? NSString
}
var localizedName: String! {
let resourceValues = fileURL.resourceValuesForKeys([NSURLLocalizedNameKey], error: nil)
return resourceValues[NSURLLocalizedNameKey] as? NSString
}
var icon: NSImage! {
let resourceValues = fileURL.resourceValuesForKeys([NSURLEffectiveIconKey], error: nil)
return resourceValues[NSURLEffectiveIconKey] as? NSImage
}
var dateOfCreation: NSDate! {
let resourceValues = self.fileURL.resourceValuesForKeys([NSURLCreationDateKey], error: nil)
return resourceValues[NSURLCreationDateKey] as? NSDate
}
var dateOfLastModification: NSDate! {
let resourceValues = fileURL.resourceValuesForKeys([NSURLContentModificationDateKey], error: nil)
return resourceValues[NSURLContentModificationDateKey] as? NSDate
}
var typeIdentifier: String! {
let resourceValues = fileURL.resourceValuesForKeys([NSURLTypeIdentifierKey], error: nil)
return resourceValues[NSURLTypeIdentifierKey] as? NSString
}
var isDirectory: String! {
let resourceValues = fileURL.resourceValuesForKeys([NSURLIsDirectoryKey], error: nil)
return resourceValues[NSURLIsDirectoryKey] as? NSString
}
var children: [FileSystemItem] {
var childs: [FileSystemItem] = []
var isDirectory: ObjCBool = ObjCBool(1)
let fileManager = NSFileManager.defaultManager()
var checkValidation = NSFileManager.defaultManager()
if (checkValidation.fileExistsAtPath(fileURL.relativePath)) {
if let itemURLs = fileManager.contentsOfDirectoryAtURL(fileURL, includingPropertiesForKeys:propertyKeys, options:.SkipsHiddenFiles, error:nil) {
for fsItemURL in itemURLs as [NSURL] {
if (fileManager.fileExistsAtPath(fsItemURL.relativePath, isDirectory: &isDirectory))
{
if(isDirectory == true) {
let checkItem = FileSystemItem(fileURL: fsItemURL)
childs.append(checkItem)
}
}
}
}
}
return childs
}
init (fileURL: NSURL) {
self.fileURL = fileURL
}
func hasChildren() -> Bool {
return self.children.count > 0
}
}
class OutlineViewController : NSObject, NSOutlineViewDataSource {
let rootFolder : String = "/"
let rootfsItem : FileSystemItem
let fsItemURL : NSURL
let propertyKeys = [NSURLLocalizedNameKey, NSURLEffectiveIconKey, NSURLIsPackageKey, NSURLIsDirectoryKey,NSURLTypeIdentifierKey]
init() {
self.fsItemURL = NSURL.fileURLWithPath(rootFolder)
self.rootfsItem = FileSystemItem(fileURL: fsItemURL)
for fsItem in rootfsItem.children as [FileSystemItem] {
for fsSubItem in fsItem.children as [FileSystemItem] {
println("\(fsItem.name) - \(fsSubItem.name)")
}
}
}
func outlineView(outlineView: NSOutlineView!, numberOfChildrenOfItem item: AnyObject!) -> Int {
if let theItem: AnyObject = item {
let tmpfsItem: FileSystemItem = item as FileSystemItem
return tmpfsItem.children.count
}
return 1
}
func outlineView(outlineView: NSOutlineView!, isItemExpandable item: AnyObject!) -> Bool {
if let theItem: AnyObject = item {
let tmpfsItem: FileSystemItem = item as FileSystemItem
return tmpfsItem.hasChildren()
}
return false
}
func outlineView(outlineView: NSOutlineView!, child index: Int, ofItem item: AnyObject!) -> AnyObject! {
if let theItem: AnyObject = item {
let tmpfsItem: FileSystemItem = item as FileSystemItem
return tmpfsItem.children[index]
}
return rootfsItem
}
func outlineView(outlineView: NSOutlineView!, objectValueForTableColumn tableColumn: NSTableColumn!, byItem item: AnyObject!) -> AnyObject! {
if let theItem: AnyObject = item {
let tmpfsItem: FileSystemItem = item as FileSystemItem
return tmpfsItem.localizedName
}
return "-empty-"
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
#IBOutlet var window: NSWindow
func applicationDidFinishLaunching(aNotification: NSNotification?) {
// Insert code here to initialize your application
}
func applicationWillTerminate(aNotification: NSNotification?) {
// Insert code here to tear down your application
}
}
Any hints ?
I had a similar problem with EXC_BAD_ACCESS on an NSOutlineView - with an NSOutlineViewDataSource. The same behaviour of as soon as the node was expanded, the data was displayed then the crash occurred. Some profiling in instruments showed that somewhere a Zombie object was created, and then the Outline view tried to access it.
I think this is a bug - but I managed to get around it by changing all Swift 'Strings' to 'NSStrings'. This may have to be done for all Swift types if you are using them.
In order to ensure everything was an NSString, I had to declare constants within the class such as:
var empty_string : NSString = ""
Because anytime I fed it a Swift string all hell broke loose. Oh well hopefully this will be fixed in the future!
So, just to clarify what is going on. NSOutlineView does not retain objects that it is given for its "model"; it was always expected that the client would retain them. For ARC code, this doesn't work well, because if you return a new instance to the NSOutlineView methods the object will not be retained by anything and will quickly be freed. Then subsequent outlineView delegate methods the touch these objects will lead to crashes. The solution to that is to retain the objects yourself in your own array.
Note that the objects returned from objectValueForTableColumn are retained by the NSControl's objectValue.
Back to Swift: As Thomas noted the objects have to be objc objects since they are bridged to an objc class. A Swift string is implicitly bridged to a temporary NSString. This leads to a crash because of the above issue, since nothing retains the NSString instance. That is why maintaining an array of NSStrings "solves" this problem.
The solution would be for NSOutlineView to have an option to retain the items given to it. Please consider logging a bug request for it to do this through bugreporter.apple.com
Thanks,
corbin (I work on NSOutlineView)
It seems that
outlineView(outlineView: NSOutlineView!, objectValueForTableColumn tableColumn: NSTableColumn!, byItem item: AnyObject!) -> AnyObject!
needs to return an object that conforms to obj-c protocol. So you can return
#objc class MyClass {
...
}
(or NSString and the like). But not native Swift stuff like String or Array etc.
I believe one of the problems going on here is the fact that the "children" array is getting replaced every time the children property is accessed.
I think this causes some weak references inside the NSOutlineView to break when it queries the DataSource for information.
If you cache the "children" and access the cache to compute "numberOfChildren" and "getChildForIndex" you should see an improvement.
In Swift 3.0 I used the following code, which compiles and runs without problems. It is far away from being complete but a step in the right direction, since I am trying to translate TreeTest into Swift.
import Cocoa
import Foundation
class FileSystemItem: NSObject {
let propertyKeys: [URLResourceKey] = [.localizedNameKey, .effectiveIconKey, .isDirectoryKey, .typeIdentifierKey]
var fileURL: URL
var name: String! {
let resourceValues = try! fileURL.resourceValues(forKeys: [.nameKey])
return resourceValues.name
}
var localizedName: String! {
let resourceValues = try! fileURL.resourceValues(forKeys: [.localizedNameKey])
return resourceValues.localizedName
}
var icon: NSImage! {
let resourceValues = try! fileURL.resourceValues(forKeys: [.effectiveIconKey])
return resourceValues.effectiveIcon as? NSImage
}
var dateOfCreation: Date! {
let resourceValues = try! fileURL.resourceValues(forKeys: [.creationDateKey])
return resourceValues.creationDate
}
var dateOfLastModification: Date! {
let resourceValues = try! fileURL.resourceValues(forKeys: [.contentModificationDateKey])
return resourceValues.contentAccessDate
}
var typeIdentifier: String! {
let resourceValues = try! fileURL.resourceValues(forKeys: [.typeIdentifierKey])
return resourceValues.typeIdentifier
}
var isDirectory: Bool! {
let resourceValues = try! fileURL.resourceValues(forKeys: [.isDirectoryKey])
return resourceValues.isDirectory
}
init(url: Foundation.URL) {
self.fileURL = url
}
var children: [FileSystemItem] {
var childs: [FileSystemItem] = []
let fileManager = FileManager.default
// show no hidden Files (if you want this, comment out next line)
// let options = FileManager.DirectoryEnumerationOptions.skipsHiddenFiles
var directoryURL = ObjCBool(false)
let validURL = fileManager.fileExists(atPath: fileURL.relativePath, isDirectory: &directoryURL)
if (validURL && directoryURL.boolValue) {
// contents of directory
do {
let childURLs = try
fileManager.contentsOfDirectory(at: fileURL, includingPropertiesForKeys: propertyKeys, options: [])
for childURL in childURLs {
let child = FileSystemItem(url: childURL)
childs.append(child)
}
}
catch {
print("Unexpected error occured: \(error).")
}
}
return childs
}
func hasChildren() -> Bool {
return self.children.count > 0
}
}
class OutLineViewController: NSViewController, NSOutlineViewDelegate, NSOutlineViewDataSource {
#IBOutlet weak var outlineView: NSOutlineView!
#IBOutlet weak var pathController: NSPathControl!
var fileSystemItemURL: URL!
let propertyKeys: [URLResourceKey] = [.localizedNameKey, .effectiveIconKey, .isDirectoryKey, .typeIdentifierKey]
var rootfileSystemItem: FileSystemItem!
var rootURL: URL!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let userDirectoryURL = URL(fileURLWithPath: NSHomeDirectory())
// directory "Pictures" is set as root
let rootURL = userDirectoryURL.appendingPathComponent("Pictures", isDirectory: true)
self.pathController.url = rootURL
self.rootfileSystemItem = FileSystemItem(url: rootURL)
for fileSystemItem in rootfileSystemItem.children as [FileSystemItem] {
for subItem in fileSystemItem.children as [FileSystemItem] {
print("\(fileSystemItem.name) - \(subItem.name)")
}
}
//FileSystemItem.rootItemWithPath(self.pathControl.URL.path)
//self.searchForFilesInDirectory(picturesPath)
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
#IBAction func pathControllerAction(_ sender: NSPathControl) {
print("controller clicked")
}
// MARK: - outline data source methods
func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
if let fileSystemItem = item as? FileSystemItem {
return fileSystemItem.children.count
}
return 1
}
func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
if let fileSystemItem = item as? FileSystemItem {
return fileSystemItem.hasChildren()
}
return false
}
func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
if let fileSystemItem = item as? FileSystemItem {
return fileSystemItem.children[index]
}
return rootfileSystemItem
}
func outlineView(_ outlineView: NSOutlineView, objectValueFor tableColumn: NSTableColumn?, byItem item: Any?) -> Any? {
if let fileSystemItem = item as? FileSystemItem {
switch tableColumn?.identifier {
case "tree"?:
return fileSystemItem.localizedName
case "coordinate"?:
return " empty "
default:
break
}
}
return " -empty- "
}
// MARK: - outline view delegate methods
func outlineView(_ outlineView: NSOutlineView, shouldEdit tableColumn: NSTableColumn?, item: Any) -> Bool {
return false
}
}
With a new edit the outline view now shows all files and directories. You can influence the appearance in the children section in class FileSystemItem.