// // UtilityExtensions.swift // EmojiArt // // Created by CS193p Instructor on 4/26/21. // Copyright © 2021 Stanford University. All rights reserved. // import SwiftUI // in a Collection of Identifiables // we often might want to find the element that has the same id // as an Identifiable we already have in hand // we name this index(matching:) instead of firstIndex(matching:) // because we assume that someone creating a Collection of Identifiable // is usually going to have only one of each Identifiable thing in there // (though there's nothing to restrict them from doing so; it's just a naming choice) extension Collection where Element: Identifiable { func index(matching element: Element) -> Self.Index? { firstIndex(where: { $0.id == element.id }) } } // we could do the same thing when it comes to removing an element // but we have to add that to a different protocol // because Collection works for immutable collections of things // the "mutable" one is RangeReplaceableCollection // not only could we add remove // but we could add a subscript which takes a copy of one of the elements // and uses its Identifiable-ness to subscript into the Collection // this is an awesome way to create Bindings into an Array in a ViewModel // (since any Published var in an ObservableObject can be bound to via $) // (even vars on that Published var or subscripts on that var) // (or subscripts on vars on that var, etc.) extension RangeReplaceableCollection where Element: Identifiable { mutating func remove(_ element: Element) { if let index = index(matching: element) { remove(at: index) } } subscript(_ element: Element) -> Element { get { if let index = index(matching: element) { return self[index] } else { return element } } set { if let index = index(matching: element) { replaceSubrange(index...index, with: [newValue]) } } } } // if you use a Set to represent the selection of emoji in HW5 // then you might find this syntactic sugar function to be of use extension Set where Element: Identifiable { mutating func toggleMembership(of element: Element) { if let index = index(matching: element) { remove(at: index) } else { insert(element) } } } // some extensions to String and Character // to help us with managing our Strings of emojis // we want them to be "emoji only" // (thus isEmoji below) // and we don't want them to have repeated emojis // (thus withNoRepeatedCharacters below) extension String { var withNoRepeatedCharacters: String { var uniqued = "" for ch in self { if !uniqued.contains(ch) { uniqued.append(ch) } } return uniqued } } extension Character { var isEmoji: Bool { // Swift does not have a way to ask if a Character isEmoji // but it does let us check to see if our component scalars isEmoji // unfortunately unicode allows certain scalars (like 1) // to be modified by another scalar to become emoji (e.g. 1️⃣) // so the scalar "1" will report isEmoji = true // so we can't just check to see if the first scalar isEmoji // the quick and dirty here is to see if the scalar is at least the first true emoji we know of // (the start of the "miscellaneous items" section) // or check to see if this is a multiple scalar unicode sequence // (e.g. a 1 with a unicode modifier to force it to be presented as emoji 1️⃣) if let firstScalar = unicodeScalars.first, firstScalar.properties.isEmoji { return (firstScalar.value >= 0x238d || unicodeScalars.count > 1) } else { return false } } } // extracting the actual url to an image from a url that might contain other info // (essentially looking for the imgurl key) // imgurl is a "well known" key that can be embedded in a url that says what the actual image url is extension URL { var imageURL: URL { for query in query?.components(separatedBy: "&") ?? [] { let queryComponents = query.components(separatedBy: "=") if queryComponents.count == 2 { if queryComponents[0] == "imgurl", let url = URL(string: queryComponents[1].removingPercentEncoding ?? "") { return url } } } return baseURL ?? self } } // convenience functions for adding/subtracting CGPoints and CGSizes // might come in handy when doing gesture handling // because we do a lot of converting between coordinate systems and such // notice that type types of the lhs and rhs arguments vary below // thus you can offset a CGPoint by the width and height of a CGSize, for example extension DragGesture.Value { var distance: CGSize { location - startLocation } } extension CGRect { var center: CGPoint { CGPoint(x: midX, y: midY) } } extension CGPoint { static func -(lhs: Self, rhs: Self) -> CGSize { CGSize(width: lhs.x - rhs.x, height: lhs.y - rhs.y) } static func +(lhs: Self, rhs: CGSize) -> CGPoint { CGPoint(x: lhs.x + rhs.width, y: lhs.y + rhs.height) } static func -(lhs: Self, rhs: CGSize) -> CGPoint { CGPoint(x: lhs.x - rhs.width, y: lhs.y - rhs.height) } static func *(lhs: Self, rhs: CGFloat) -> CGPoint { CGPoint(x: lhs.x * rhs, y: lhs.y * rhs) } static func /(lhs: Self, rhs: CGFloat) -> CGPoint { CGPoint(x: lhs.x / rhs, y: lhs.y / rhs) } } extension CGSize { // the center point of an area that is our size var center: CGPoint { CGPoint(x: width/2, y: height/2) } static func +(lhs: Self, rhs: Self) -> CGSize { CGSize(width: lhs.width + rhs.width, height: lhs.height + rhs.height) } static func -(lhs: Self, rhs: Self) -> CGSize { CGSize(width: lhs.width - rhs.width, height: lhs.height - rhs.height) } static func *(lhs: Self, rhs: CGFloat) -> CGSize { CGSize(width: lhs.width * rhs, height: lhs.height * rhs) } static func /(lhs: Self, rhs: CGFloat) -> CGSize { CGSize(width: lhs.width/rhs, height: lhs.height/rhs) } } // add RawRepresentable protocol conformance to CGSize and CGFloat // so that they can be used with @SceneStorage // we do this by first providing default implementations of rawValue and init(rawValue:) // in RawRepresentable when the thing in question is Codable (which both CGFloat and CGSize are) // then all it takes to make something that is Codable be RawRepresentable is to declare it to be so // (it will then get the default implementions needed to be a RawRepresentable) extension RawRepresentable where Self: Codable { public var rawValue: String { if let json = try? JSONEncoder().encode(self), let string = String(data: json, encoding: .utf8) { return string } else { return "" } } public init?(rawValue: String) { if let value = try? JSONDecoder().decode(Self.self, from: Data(rawValue.utf8)) { self = value } else { return nil } } } extension CGSize: RawRepresentable { } extension CGFloat: RawRepresentable { } // convenience functions for [NSItemProvider] (i.e. array of NSItemProvider) // makes the code for loading objects from the providers a bit simpler // NSItemProvider is a holdover from the Objective-C (i.e. pre-Swift) world // you can tell by its very name (starts with NS) // so unfortunately, dealing with this API is a little bit crufty // thus I recommend you just accept that these loadObjects functions will work and move on // it's a rare case where trying to dive in and understand what's going on here // would probably not be a very efficient use of your time // (though I'm certainly not going to say you shouldn't!) // (just trying to help you optimize your valuable time this quarter) extension Array where Element == NSItemProvider { func loadObjects(ofType theType: T.Type, firstOnly: Bool = false, using load: @escaping (T) -> Void) -> Bool where T: NSItemProviderReading { if let provider = first(where: { $0.canLoadObject(ofClass: theType) }) { provider.loadObject(ofClass: theType) { object, error in if let value = object as? T { DispatchQueue.main.async { load(value) } } } return true } return false } func loadObjects(ofType theType: T.Type, firstOnly: Bool = false, using load: @escaping (T) -> Void) -> Bool where T: _ObjectiveCBridgeable, T._ObjectiveCType: NSItemProviderReading { if let provider = first(where: { $0.canLoadObject(ofClass: theType) }) { let _ = provider.loadObject(ofClass: theType) { object, error in if let value = object { DispatchQueue.main.async { load(value) } } } return true } return false } func loadFirstObject(ofType theType: T.Type, using load: @escaping (T) -> Void) -> Bool where T: NSItemProviderReading { loadObjects(ofType: theType, firstOnly: true, using: load) } func loadFirstObject(ofType theType: T.Type, using load: @escaping (T) -> Void) -> Bool where T: _ObjectiveCBridgeable, T._ObjectiveCType: NSItemProviderReading { loadObjects(ofType: theType, firstOnly: true, using: load) } }