cs193p-emojiart/EmojiArt/UtilityExtensions.swift
Ching 89d38faea4 feat(view, viewmodel, model): 增加 document 和 palette,增加手势
1. 可以从另一个 app 中拖入图片或图片链接作为背景
2. 可以从 palette 中拖入 emoji
3. 可以拖动和双击缩放背景图

Signed-off-by: Ching <loooching@gmail.com>
2023-02-20 21:45:33 +08:00

255 lines
9.5 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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)
}
}