www.youtube.com/watch?v=0i152oA3T3s&feature=youtu.be
www.youtube.com/watch?v=CKexGQuIO7E&feature=youtu.be
9. Slides.pdf
0.83MB
10. Slides.pdf
0.72MB
Assignment.pdf
0.69MB
EmojiArtL10.zip
0.08MB
@Binding
@EnvironmentObject
@Environment
TextField
Form
zIndex
@EnvironmentObject
@EnvironmentObject
소스
EmojiArtDocumentStore.swift
import SwiftUI
import Combine
class EmojiArtDocumentStore: ObservableObject
{
let name: String
func name(for document: EmojiArtDocument) -> String {
if documentNames[document] == nil {
documentNames[document] = "Untitled"
}
return documentNames[document]!
}
func setName(_ name: String, for document: EmojiArtDocument) {
documentNames[document] = name
}
var documents: [EmojiArtDocument] {
documentNames.keys.sorted { documentNames[$0]! < documentNames[$1]! }
}
func addDocument(named name: String = "Untitled") {
documentNames[EmojiArtDocument()] = name
}
func removeDocument(_ document: EmojiArtDocument) {
documentNames[document] = nil
}
@Published private var documentNames = [EmojiArtDocument:String]()
private var autosave: AnyCancellable?
init(named name: String = "Emoji Art") {
self.name = name
let defaultsKey = "EmojiArtDocumentStore.\(name)"
documentNames = Dictionary(fromPropertyList: UserDefaults.standard.object(forKey: defaultsKey))
autosave = $documentNames.sink { names in
UserDefaults.standard.set(names.asPropertyList, forKey: defaultsKey)
}
}
}
extension Dictionary where Key == EmojiArtDocument, Value == String {
var asPropertyList: [String:String] {
var uuidToName = [String:String]()
for (key, value) in self {
uuidToName[key.id.uuidString] = value
}
return uuidToName
}
init(fromPropertyList plist: Any?) {
self.init()
let uuidToName = plist as? [String:String] ?? [:]
for uuid in uuidToName.keys {
self[EmojiArtDocument(id: UUID(uuidString: uuid))] = uuidToName[uuid]
}
}
}
EmojiArtDocumentChooser.swift
import SwiftUI
struct EmojiArtDocumentChooser: View {
@EnvironmentObject var store: EmojiArtDocumentStore
@State private var editMode: EditMode = .inactive
var body: some View {
NavigationView {
List {
ForEach(store.documents) { document in
NavigationLink(destination: EmojiArtDocumentView(document: document)
.navigationBarTitle(self.store.name(for: document))
) {
EditableText(self.store.name(for: document), isEditing: self.editMode.isEditing) { name in
self.store.setName(name, for: document)
}
}
}
.onDelete { indexSet in
indexSet.map { self.store.documents[$0] }.forEach { document in
self.store.removeDocument(document)
}
}
}
.navigationBarTitle(self.store.name)
.navigationBarItems(
leading: Button(action: {
self.store.addDocument()
}, label: {
Image(systemName: "plus").imageScale(.large)
}),
trailing: EditButton()
)
.environment(\.editMode, $editMode)
}
}
}
struct EmojiArtDocumentChooser_Previews: PreviewProvider {
static var previews: some View {
EmojiArtDocumentChooser()
}
}
EmojiArtDocument.swift
import SwiftUI
import Combine
class EmojiArtDocument: ObservableObject, Hashable, Identifiable
{
static func == (lhs: EmojiArtDocument, rhs: EmojiArtDocument) -> Bool {
lhs.id == rhs.id
}
let id: UUID
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
static let palette: String = "⭐️⛈🍎🌏🥨⚾️"
@Published private var emojiArt: EmojiArt
private var autosaveCancellable: AnyCancellable?
init(id: UUID? = nil) {
self.id = id ?? UUID()
let defaultsKey = "EmojiArtDocument.\(self.id.uuidString)"
emojiArt = EmojiArt(json: UserDefaults.standard.data(forKey: defaultsKey)) ?? EmojiArt()
autosaveCancellable = $emojiArt.sink { emojiArt in
UserDefaults.standard.set(emojiArt.json, forKey: defaultsKey)
}
fetchBackgroundImageData()
}
@Published private(set) var backgroundImage: UIImage?
@Published var steadyStateZoomScale: CGFloat = 1.0
@Published var steadyStatePanOffset: CGSize = .zero
var emojis: [EmojiArt.Emoji] { emojiArt.emojis }
// MARK: - Intent(s)
func addEmoji(_ emoji: String, at location: CGPoint, size: CGFloat) {
emojiArt.addEmoji(emoji, x: Int(location.x), y: Int(location.y), size: Int(size))
}
func moveEmoji(_ emoji: EmojiArt.Emoji, by offset: CGSize) {
if let index = emojiArt.emojis.firstIndex(matching: emoji) {
emojiArt.emojis[index].x += Int(offset.width)
emojiArt.emojis[index].y += Int(offset.height)
}
}
func scaleEmoji(_ emoji: EmojiArt.Emoji, by scale: CGFloat) {
if let index = emojiArt.emojis.firstIndex(matching: emoji) {
emojiArt.emojis[index].size = Int((CGFloat(emojiArt.emojis[index].size) * scale).rounded(.toNearestOrEven))
}
}
var backgroundURL: URL? {
get {
emojiArt.backgroundURL
}
set {
emojiArt.backgroundURL = newValue?.imageURL
fetchBackgroundImageData()
}
}
private var fetchImageCancellable: AnyCancellable?
private func fetchBackgroundImageData() {
backgroundImage = nil
if let url = self.emojiArt.backgroundURL {
fetchImageCancellable?.cancel()
fetchImageCancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { data, urlResponse in UIImage(data: data) }
.receive(on: DispatchQueue.main)
.replaceError(with: nil)
.assign(to: \.backgroundImage, on: self)
}
}
}
extension EmojiArt.Emoji {
var fontSize: CGFloat { CGFloat(self.size) }
var location: CGPoint { CGPoint(x: CGFloat(x), y: CGFloat(y)) }
}
EmojiArtDocumentView.swift
import SwiftUI
struct EmojiArtDocumentView: View {
@ObservedObject var document: EmojiArtDocument
@State private var chosenPalette: String = ""
init(document: EmojiArtDocument) {
self.document = document
_chosenPalette = State(wrappedValue: self.document.defaultPalette)
}
var body: some View {
VStack {
HStack {
PaletteChooser(document: document, chosenPalette: $chosenPalette)
ScrollView(.horizontal) {
HStack {
ForEach(chosenPalette.map { String($0) }, id: \.self) { emoji in
Text(emoji)
.font(Font.system(size: self.defaultEmojiSize))
.onDrag { NSItemProvider(object: emoji as NSString) }
}
}
}
}
GeometryReader { geometry in
ZStack {
Color.white.overlay(
OptionalImage(uiImage: self.document.backgroundImage)
.scaleEffect(self.zoomScale)
.offset(self.panOffset)
)
.gesture(self.doubleTapToZoom(in: geometry.size))
if self.isLoading {
Image(systemName: "hourglass").imageScale(.large).spinning()
} else {
ForEach(self.document.emojis) { emoji in
Text(emoji.text)
.font(animatableWithSize: emoji.fontSize * self.zoomScale)
.position(self.position(for: emoji, in: geometry.size))
}
}
}
.clipped()
.gesture(self.panGesture())
.gesture(self.zoomGesture())
.edgesIgnoringSafeArea([.horizontal, .bottom])
.onReceive(self.document.$backgroundImage) { image in
self.zoomToFit(image, in: geometry.size)
}
.onDrop(of: ["public.image","public.text"], isTargeted: nil) { providers, location in
var location = geometry.convert(location, from: .global)
location = CGPoint(x: location.x - geometry.size.width/2, y: location.y - geometry.size.height/2)
location = CGPoint(x: location.x - self.panOffset.width, y: location.y - self.panOffset.height)
location = CGPoint(x: location.x / self.zoomScale, y: location.y / self.zoomScale)
return self.drop(providers: providers, at: location)
}
.navigationBarItems(trailing: Button(action: {
if let url = UIPasteboard.general.url, url != self.document.backgroundURL {
self.confirmBackgroundPaste = true
} else {
self.explainBackgroundPaste = true
}
}, label: {
Image(systemName: "doc.on.clipboard").imageScale(.large)
.alert(isPresented: self.$explainBackgroundPaste) {
return Alert(
title: Text("Paste Background"),
message: Text("Copy the URL of an image to the clip board and touch this button to make it the background of your document."),
dismissButton: .default(Text("OK"))
)
}
}))
}
.zIndex(-1)
}
.alert(isPresented: self.$confirmBackgroundPaste) {
Alert(
title: Text("Paste Background"),
message: Text("Replace your background with \(UIPasteboard.general.url?.absoluteString ?? "nothing")?."),
primaryButton: .default(Text("OK")) {
self.document.backgroundURL = UIPasteboard.general.url
},
secondaryButton: .cancel()
)
}
}
@State private var explainBackgroundPaste = false
@State private var confirmBackgroundPaste = false
var isLoading: Bool {
document.backgroundURL != nil && document.backgroundImage == nil
}
@GestureState private var gestureZoomScale: CGFloat = 1.0
private var zoomScale: CGFloat {
document.steadyStateZoomScale * gestureZoomScale
}
private func zoomGesture() -> some Gesture {
MagnificationGesture()
.updating($gestureZoomScale) { latestGestureScale, gestureZoomScale, transaction in
gestureZoomScale = latestGestureScale
}
.onEnded { finalGestureScale in
self.document.steadyStateZoomScale *= finalGestureScale
}
}
@GestureState private var gesturePanOffset: CGSize = .zero
private var panOffset: CGSize {
(document.steadyStatePanOffset + gesturePanOffset) * zoomScale
}
private func panGesture() -> some Gesture {
DragGesture()
.updating($gesturePanOffset) { latestDragGestureValue, gesturePanOffset, transaction in
gesturePanOffset = latestDragGestureValue.translation / self.zoomScale
}
.onEnded { finalDragGestureValue in
self.document.steadyStatePanOffset = self.document.steadyStatePanOffset + (finalDragGestureValue.translation / self.zoomScale)
}
}
private func doubleTapToZoom(in size: CGSize) -> some Gesture {
TapGesture(count: 2)
.onEnded {
withAnimation {
self.zoomToFit(self.document.backgroundImage, in: size)
}
}
}
private func zoomToFit(_ image: UIImage?, in size: CGSize) {
if let image = image, image.size.width > 0, image.size.height > 0, size.height > 0, size.width > 0 {
let hZoom = size.width / image.size.width
let vZoom = size.height / image.size.height
self.document.steadyStatePanOffset = .zero
self.document.steadyStateZoomScale = min(hZoom, vZoom)
}
}
private func position(for emoji: EmojiArt.Emoji, in size: CGSize) -> CGPoint {
var location = emoji.location
location = CGPoint(x: location.x * zoomScale, y: location.y * zoomScale)
location = CGPoint(x: location.x + size.width/2, y: location.y + size.height/2)
location = CGPoint(x: location.x + panOffset.width, y: location.y + panOffset.height)
return location
}
private func drop(providers: [NSItemProvider], at location: CGPoint) -> Bool {
var found = providers.loadFirstObject(ofType: URL.self) { url in
self.document.backgroundURL = url
}
if !found {
found = providers.loadObjects(ofType: String.self) { string in
self.document.addEmoji(string, at: location, size: self.defaultEmojiSize)
}
}
return found
}
private let defaultEmojiSize: CGFloat = 40
}
EmojiArtDocument+Palette.swift
import Foundation
// MARK: - Palette Extension
extension EmojiArtDocument
{
private static let PalettesKey = "EmojiArtDocument.PalettesKey"
// even though this is an instance var, it is shared across instances
// and is also persistent across application launches
private(set) var paletteNames: [String:String] {
get {
UserDefaults.standard.object(forKey: EmojiArtDocument.PalettesKey) as? [String:String] ?? [
"😀😅😂😇🥰😉🙃😎🥳😡🤯🥶🤥😴🙄👿😷🤧🤡":"Faces",
"🍏🍎🥒🍞🥨🥓🍔🍟🍕🍰🍿☕️":"Food",
"🐶🐼🐵🙈🙉🙊🦆🐝🕷🐟🦓🐪🦒🦨":"Animals",
"⚽️🏈⚾️🎾🏐🏓⛳️🥌⛷🚴♂️🎳🎼🎭🪂":"Activities"
]
}
set {
UserDefaults.standard.set(newValue, forKey: EmojiArtDocument.PalettesKey)
objectWillChange.send()
}
}
var sortedPalettes: [String] {
paletteNames.keys.sorted(by: { paletteNames[$0]! < paletteNames[$1]! })
}
var defaultPalette: String {
sortedPalettes.first ?? "⚠️"
}
func renamePalette(_ palette: String, to name: String) {
paletteNames[palette] = name
}
func addPalette(_ palette: String, named name: String) {
paletteNames[name] = palette
}
func removePalette(named name: String) {
paletteNames[name] = nil
}
@discardableResult
func addEmoji(_ emoji: String, toPalette palette: String) -> String {
return changePalette(palette, to: (emoji + palette).uniqued())
}
@discardableResult
func removeEmoji(_ emojisToRemove: String, fromPalette palette: String) -> String {
return changePalette(palette, to: palette.filter { !emojisToRemove.contains($0) })
}
private func changePalette(_ palette: String, to newPalette: String) -> String {
let name = paletteNames[palette] ?? ""
paletteNames[palette] = nil
paletteNames[newPalette] = name
return newPalette
}
func palette(after otherPalette: String) -> String {
palette(offsetBy: +1, from: otherPalette)
}
func palette(before otherPalette: String) -> String {
palette(offsetBy: -1, from: otherPalette)
}
private func palette(offsetBy offset: Int, from otherPalette: String) -> String {
if let currentIndex = mostLikelyIndex(of: otherPalette) {
let newIndex = (currentIndex + (offset >= 0 ? offset : sortedPalettes.count - abs(offset) % sortedPalettes.count)) % sortedPalettes.count
return sortedPalettes[newIndex]
} else {
return defaultPalette
}
}
// this is a trick to make the code in the demo a little bit simpler
// in the real world, we'd want palettes to be Identifiable
// here we're simply guessing at that 😀
private func mostLikelyIndex(of palette: String) -> Int? {
let paletteSet = Set(palette)
var best: (index: Int, score: Int)?
let palettes = sortedPalettes
for index in palettes.indices {
let score = paletteSet.intersection(Set(palettes[index])).count
if score > (best?.score ?? 0) {
best = (index, score)
}
}
return best?.index
}
}
EmojiArt.swift
import Foundation
struct EmojiArt: Codable {
var backgroundURL: URL?
var emojis = [Emoji]()
struct Emoji: Identifiable, Codable, Hashable {
let text: String
var x: Int
var y: Int
var size: Int
let id: Int
fileprivate init(text: String, x: Int, y: Int, size: Int, id: Int) {
self.text = text
self.x = x
self.y = y
self.size = size
self.id = id
}
}
var json: Data? {
return try? JSONEncoder().encode(self)
}
init?(json: Data?) {
if json != nil, let newEmojiArt = try? JSONDecoder().decode(EmojiArt.self, from: json!) {
self = newEmojiArt
} else {
return nil
}
}
init() { }
private var uniqueEmojiId = 0
mutating func addEmoji(_ text: String, x: Int, y: Int, size: Int) {
uniqueEmojiId += 1
emojis.append(Emoji(text: text, x: x, y: y, size: size, id: uniqueEmojiId))
}
}
EmojiArtExtensions.swift
import SwiftUI
extension Collection where Element: Identifiable {
func firstIndex(matching element: Element) -> Self.Index? {
firstIndex(where: { $0.id == element.id })
}
func contains(matching element: Element) -> Bool {
self.contains(where: { $0.id == element.id })
}
}
extension URL {
var imageURL: URL {
// check to see if there is an embedded imgurl reference
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 self.baseURL ?? self
}
}
extension GeometryProxy {
func convert(_ point: CGPoint, from coordinateSpace: CoordinateSpace) -> CGPoint {
let frame = self.frame(in: coordinateSpace)
return CGPoint(x: point.x-frame.origin.x, y: point.y-frame.origin.y)
}
}
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 = self.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 = self.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 {
self.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 {
self.loadObjects(ofType: theType, firstOnly: true, using: load)
}
}
extension Data {
var utf8: String? { String(data: self, encoding: .utf8 ) }
}
extension DragGesture.Value {
var distance: CGSize { location - startLocation }
}
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 {
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)
}
}
extension Dictionary where Value: Equatable {
func anyKey(forValue value: Value) -> Key? {
for key in keys {
if self[key] == value {
return key
}
}
return nil
}
}
extension String {
func uniqued() -> String {
var uniqued = ""
for ch in self {
if !uniqued.contains(ch) {
uniqued.append(ch)
}
}
return uniqued
}
}
EditableText.swift
import SwiftUI
struct EditableText: View {
var text: String = ""
var isEditing: Bool
var onChanged: (String) -> Void
init(_ text: String, isEditing: Bool, onChanged: @escaping (String) -> Void) {
self.text = text
self.isEditing = isEditing
self.onChanged = onChanged
}
@State private var editableText: String = ""
var body: some View {
ZStack(alignment: .leading) {
TextField(text, text: $editableText, onEditingChanged: { began in
self.callOnChangedIfChanged()
})
.opacity(isEditing ? 1 : 0)
.disabled(!isEditing)
if !isEditing {
Text(text)
.opacity(isEditing ? 0 : 1)
.onAppear {
// any time we move from editable to non-editable
// we want to report any changes that happened to the text
// while were editable
// (i.e. we never "abandon" changes)
self.callOnChangedIfChanged()
}
}
}
.onAppear { self.editableText = self.text }
}
func callOnChangedIfChanged() {
if editableText != text {
onChanged(editableText)
}
}
}
PaletteChooser.swift
import SwiftUI
struct PaletteChooser: View {
@ObservedObject var document: EmojiArtDocument
@Binding var chosenPalette: String
@State private var showPaletteEditor = false
var body: some View {
HStack {
Stepper(onIncrement: {
self.chosenPalette = self.document.palette(after: self.chosenPalette)
}, onDecrement: {
self.chosenPalette = self.document.palette(before: self.chosenPalette)
}, label: { EmptyView() })
Text(self.document.paletteNames[self.chosenPalette] ?? "")
Image(systemName: "keyboard").imageScale(.large)
.onTapGesture {
self.showPaletteEditor = true
}
.popover(isPresented: $showPaletteEditor) {
PaletteEditor(chosenPalette: self.$chosenPalette, isShowing: self.$showPaletteEditor)
.environmentObject(self.document)
.frame(minWidth: 300, minHeight: 500)
}
}
.fixedSize(horizontal: true, vertical: false)
}
}
struct PaletteEditor: View {
@EnvironmentObject var document: EmojiArtDocument
@Binding var chosenPalette: String
@Binding var isShowing: Bool
@State private var paletteName: String = ""
@State private var emojisToAdd: String = ""
var body: some View {
VStack(spacing: 0) {
ZStack {
Text("Palette Editor").font(.headline).padding()
HStack {
Spacer()
Button(action: {
self.isShowing = false
}, label: { Text("Done") }).padding()
}
}
Divider()
Form {
Section {
TextField("Palette Name", text: $paletteName, onEditingChanged: { began in
if !began {
self.document.renamePalette(self.chosenPalette, to: self.paletteName)
}
})
TextField("Add Emoji", text: $emojisToAdd, onEditingChanged: { began in
if !began {
self.chosenPalette = self.document.addEmoji(self.emojisToAdd, toPalette: self.chosenPalette)
self.emojisToAdd = ""
}
})
}
Section(header: Text("Remove Emoji")) {
Grid(chosenPalette.map { String($0) }, id: \.self) { emoji in
Text(emoji).font(Font.system(size: self.fontSize))
.onTapGesture {
self.chosenPalette = self.document.removeEmoji(emoji, fromPalette: self.chosenPalette)
}
}
.frame(height: self.height)
}
}
}
.onAppear { self.paletteName = self.document.paletteNames[self.chosenPalette] ?? "" }
}
// MARK: - Drawing Constants
var height: CGFloat {
CGFloat((chosenPalette.count - 1) / 6) * 70 + 70
}
let fontSize: CGFloat = 40
}
struct PaletteChooser_Previews: PreviewProvider {
static var previews: some View {
PaletteChooser(document: EmojiArtDocument(), chosenPalette: Binding.constant(""))
}
}
Spinning.swift
import SwiftUI
struct Spinning: ViewModifier {
@State var isVisible = false
func body(content: Content) -> some View {
content
.rotationEffect(Angle(degrees: isVisible ? 360 : 0))
.animation(Animation.linear(duration: 1).repeatForever(autoreverses: false))
.onAppear { self.isVisible = true }
}
}
extension View {
func spinning() -> some View {
self.modifier(Spinning())
}
}
OptionalImage.swift
import SwiftUI
struct OptionalImage: View {
var uiImage: UIImage?
var body: some View {
Group {
if uiImage != nil {
Image(uiImage: uiImage!)
}
}
}
}
AnimatableSystemFontModifier.swift
import SwiftUI
struct AnimatableSystemFontModifier: AnimatableModifier {
var size: CGFloat
var weight: Font.Weight = .regular
var design: Font.Design = .default
func body(content: Content) -> some View {
content.font(Font.system(size: size, weight: weight, design: design))
}
var animatableData: CGFloat {
get { size }
set { size = newValue }
}
}
extension View {
func font(animatableWithSize size: CGFloat, weight: Font.Weight = .regular, design: Font.Design = .default) -> some View {
self.modifier(AnimatableSystemFontModifier(size: size, weight: weight, design: design))
}
}
Grid.swift
import SwiftUI
extension Grid where Item: Identifiable, ID == Item.ID {
init(_ items: [Item], viewForItem: @escaping (Item) -> ItemView) {
self.init(items, id: \Item.id, viewForItem: viewForItem)
}
}
struct Grid<Item, ID, ItemView>: View where ID: Hashable, ItemView: View {
private var items: [Item]
private var id: KeyPath<Item,ID>
private var viewForItem: (Item) -> ItemView
init(_ items: [Item], id: KeyPath<Item,ID>, viewForItem: @escaping (Item) -> ItemView) {
self.items = items
self.id = id
self.viewForItem = viewForItem
}
var body: some View {
GeometryReader { geometry in
self.body(for: GridLayout(itemCount: self.items.count, in: geometry.size))
}
}
private func body(for layout: GridLayout) -> some View {
return ForEach(items, id: id) { item in
self.body(for: item, in: layout)
}
}
private func body(for item: Item, in layout: GridLayout) -> some View {
let index = items.firstIndex(where: { item[keyPath: id] == $0[keyPath: id] } )
return Group {
if index != nil {
viewForItem(item)
.frame(width: layout.itemSize.width, height: layout.itemSize.height)
.position(layout.location(ofItemAt: index!))
}
}
}
}
GridLayout.swift
import SwiftUI
struct GridLayout {
private(set) var size: CGSize
private(set) var rowCount: Int = 0
private(set) var columnCount: Int = 0
init(itemCount: Int, nearAspectRatio desiredAspectRatio: Double = 1, in size: CGSize) {
self.size = size
// if our size is zero width or height or the itemCount is not > 0
// then we have no work to do (because our rowCount & columnCount will be zero)
guard size.width != 0, size.height != 0, itemCount > 0 else { return }
// find the bestLayout
// i.e., one which results in cells whose aspectRatio
// has the smallestVariance from desiredAspectRatio
// not necessarily most optimal code to do this, but easy to follow (hopefully)
var bestLayout: (rowCount: Int, columnCount: Int) = (1, itemCount)
var smallestVariance: Double?
let sizeAspectRatio = abs(Double(size.width/size.height))
for rows in 1...itemCount {
let columns = (itemCount / rows) + (itemCount % rows > 0 ? 1 : 0)
if (rows - 1) * columns < itemCount {
let itemAspectRatio = sizeAspectRatio * (Double(rows)/Double(columns))
let variance = abs(itemAspectRatio - desiredAspectRatio)
if smallestVariance == nil || variance < smallestVariance! {
smallestVariance = variance
bestLayout = (rowCount: rows, columnCount: columns)
}
}
}
rowCount = bestLayout.rowCount
columnCount = bestLayout.columnCount
}
var itemSize: CGSize {
if rowCount == 0 || columnCount == 0 {
return CGSize.zero
} else {
return CGSize(
width: size.width / CGFloat(columnCount),
height: size.height / CGFloat(rowCount)
)
}
}
func location(ofItemAt index: Int) -> CGPoint {
if rowCount == 0 || columnCount == 0 {
return CGPoint.zero
} else {
return CGPoint(
x: (CGFloat(index % columnCount) + 0.5) * itemSize.width,
y: (CGFloat(index / columnCount) + 0.5) * itemSize.height
)
}
}
}
SceneDelegate.swift
import UIKit
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Create the SwiftUI view that provides the window contents.
let store = EmojiArtDocumentStore(named: "Emoji Art")
let contentView = EmojiArtDocumentChooser().environmentObject(store)
// EmojiArtDocumentView(document: EmojiArtDocument())
// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
func sceneDidDisconnect(_ scene: UIScene) {
// Called as the scene is being released by the system.
// This occurs shortly after the scene enters the background, or when its session is discarded.
// Release any resources associated with this scene that can be re-created the next time the scene connects.
// The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
}
func sceneDidBecomeActive(_ scene: UIScene) {
// Called when the scene has moved from an inactive state to an active state.
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
}
func sceneWillResignActive(_ scene: UIScene) {
// Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call).
}
func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
}
func sceneDidEnterBackground(_ scene: UIScene) {
// Called as the scene transitions from the foreground to the background.
// Use this method to save data, release shared resources, and store enough scene-specific state information
// to restore the scene back to its current state.
}
}
'IOS' 카테고리의 다른 글
3. IOS 강의 Enroute Picker, Core Data (0) | 2021.01.22 |
---|---|
3. IOS 강의 Gestures JSON (0) | 2021.01.19 |
3. IOS 강의 Multithreading EmojiArt (0) | 2021.01.18 |
3. IOS 강의 Animation (0) | 2021.01.18 |
3. IOS 강의 ViewBuilder Shape ViewModifier (0) | 2021.01.18 |