1. 可以从另一个 app 中拖入图片或图片链接作为背景 2. 可以从 palette 中拖入 emoji 3. 可以拖动和双击缩放背景图 Signed-off-by: Ching <loooching@gmail.com>
255 lines
9.5 KiB
Swift
255 lines
9.5 KiB
Swift
//
|
||
// 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<T>(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<T>(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<T>(ofType theType: T.Type, using load: @escaping (T) -> Void) -> Bool where T: NSItemProviderReading {
|
||
loadObjects(ofType: theType, firstOnly: true, using: load)
|
||
}
|
||
func loadFirstObject<T>(ofType theType: T.Type, using load: @escaping (T) -> Void) -> Bool where T: _ObjectiveCBridgeable, T._ObjectiveCType: NSItemProviderReading {
|
||
loadObjects(ofType: theType, firstOnly: true, using: load)
|
||
}
|
||
}
|