Lecture 10: Multithreading Demo Gestures

강의 링크

Model 에서 location은 센터를 기준으로 함 즉, (0, 0)은 정 가운데를 의미..!

  • 이렇게 하면 어떤 기기에서든 센터를 기준으로 정렬되어 호환성이 좋고,
  • 배경 이미지 또한 센터를 기준으로 위치를 정하면 돼서 편리함...!
  • 사실 9강에서 썼어야되는 내용인데 그냥 여기에 낑겨 넣는다..

# 또 드랍...근데 이제 이미지와 url을 곁들인

  • 이모지 아트에는 이모지 외에도 배경을 drag & drop 할 수 있으므로 이미지나 url이 drop 되었을 때 이를 받을 수 있어야 한다

  • 이를 위해서 기존 drop 함수found 변수를 새로 선언해서 provider 가 이미지, url, 이모지 중 무엇을 담고 있는지 순차적으로 확인한다! 강의를 듣고 나서 혼자 코드를 짜면서 왜 View 에서는 Image, URL, String 을 받게 될까가 좀 고민이었는데 생각해보니까 UI 는 당연히 우리가 아는 형태로 유저가 보낸 데이터를 받고,Model 은 해당 데이터를 UI 표현 방식과 무관한, 자신이 보관하기 가장 좋은 형태로 바꾸어 저장하며, ViewModel 이 가운데에서 ViewModel 이 원활하게 소통할 수 있도록 적절한 방식으로 데이터를 가공해주는 거였다...뭔가 MVVM 에 대해 감이 잡히는 것 같다...!

    • 그래서 예를 들어 이미지의 경우 View 에서는 Image 의 형태로, ViewModel 에서는 UIImage 의 형태로 그리고 Model 에서는 이미지를 나타내는 URL 이나 Data 의 형태로 보관하고 있는 것...!
struct EmojiArtDocumentView: View {
    private func drop(provider: [NSItemProvider], at location: CGPoint, in geometry: GeometryProxy) -> Bool {
        // returns whether it was able to load that object
        var found = provider.loadObjects(ofType: URL.self) { url in
        	// some code
        }
        
        if !found {
            found = provider.loadObjects(ofType: UIImage.self) { image in
            	...
            }
            
        }
        
        if !found {
            // if the provider contains string return true
            found = provider.loadObjects(ofType: String.self) { string in
                if let emoji = string.first, emoji.isEmoji {
                    document.addEmoji(
                        String(emoji),
                        at: convertToEmojiCoordinates(location, in: geometry),
                        size: defaultEmojiFontSize / zoomScale
                    )
                }
            }
        }
        return found
    }
}

url인 경우

  • 드래그한 이미지의 url이 출처 주소로 래핑되어있는 등 불필요한 정보가 포함되어있을 수 있으므로 이미지 url 만 추출하는 과정이 필요하다! 즉 imgurl 을 찾는 것인데, 이게 바로 url.imageURL 이 하는 역할이다!
var found = provider.loadObjects(ofType: URL.self) { url in
            document.setBackground(.url(url.imageURL))
        }
  • 사실 구체적인 과정을 따라가보려고 했는데 실패해서 정확한 과정은 나도 모르겠지만
    • imgurl 의 경우 imgurl=some%url%to%image 와 같은 형식으로 되어 있으므로
    • 먼저 불필요한 정보를 제거하기 위해 & 를 기준으로 분절한 다음 쿼리별로
    • imgurl 인지 체크하기 위해 = 로 구분해서 2개로 나뉘고 첫번째 원소가 imgurl 이라면 % 를 제거해서 imgurl 을 추출하는 것 같다!
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
                }
            }
        }
        // if the URL itself is absolute, baseURL is nil
        return baseURL ?? self
    }
}

이미지인 경우

  • 드래그한 이미지를 jpeg 형식으로 바꿔줘야 하는데 다행히 이 친구는 내장함수가 있다!
if !found {
            found = provider.loadObjects(ofType: UIImage.self) { image in
                if let data = image.jpegData(compressionQuality: 1.0 ) {
                    document.setBackground(.imageData(data))
                }
            }
}

# Model로부터 배경화면 UIImage 생성하기

  • 배경화면에 이미지를 드래그 & 드랍하면 Model 에는 해당 이미지의 URL 이나 Image Data 가 저장된다. View 에서 이미지를 나타내려면 UIImageImage 로 변환하는 작업이 필요하므로 ViewModel 에서 Model 의 데이터를 이용해 UIImage 를 구성한다...!

  • 배경화면 이미지가 바뀔 때마다 View 에 알릴 필요가 있으므로 @Published var BackgroundImage: UIImage? 와 같이 선언한다! 이때 url 로부터 이미지를 불러오는 과정을 시간이 걸릴 수 있기 때문에 computed var 로 선언할 수 없고, 매번 set 해줘야 하므로 property observerdidSet 을 사용해 Model 에서 배경화면이 바뀔 때마다 업데이트하도록 한다...!

    • 옵셔널인 이유는 url 로부터 이미지를 불러오는 데 실패하는 등 에러가 있을 수 있기 때문
class EmojiArtDocument: ObservableObject {
    
    @Published private(set) var emojiArt: EmojiArtModel {
        didSet {
            if emojiArt.background != oldValue.background {
                fetchBackgroundImageDataIfNecessary()
            }
        }
    }
    
    //syntatic sugar
    ...
    var background: EmojiArtModel.Background { emojiArt.background }
    
    
    // MARK: - Background
    @Published var backgroundImage: UIImage?
    ...
    private func fetchBackgroundImageDataIfNecessary() {
        // some code...
    }
    ...
}

UI에 영향을 주는 모든 것은 Main Queue 에서...!

  • 위에서 얘기했듯이 url 로부터 이미지를 가져오는 것은 오래걸릴 수 있기 때문에 아래와 같이 백그라운드 큐에서 작업을 수행해야 한다
  • 그러나 불러온 이미지를 배경화면으로 바꾸는 작업은 UI 에 영향을 미치므로 이미지를 불러온 다음의 일은 메인 큐에서 수행해야 한다...!
...
    private func fetchBackgroundImageDataIfNecessary() {
        backgroundImage = nil
        switch emojiArt.background {
        case .url(let url):
            // fetch the url
            backgroundImageFetchStatus = .fetching
            DispatchQueue.global(qos: .userInitiated).async {
                let imageData = try? Data(contentsOf: url)
                DispatchQueue.main.async {
                    if imageData != nil {
                        self.backgroundImage = UIImage(data: imageData!)
                    }
                }
            }
        case .imageData(let data):
            backgroundImage = UIImage(data: data)
        case .blank:
            break
        }
        
    }
...

# .self...아니..나는 약하다...weak self

  • 위에서처럼 fetchBackgroundImageDataIfNecessary 함수 에서 메인큐에 backgroundImage 를 넣으면 Reference to property 'backgroundImage' in closure requires explicit use of 'self' to make capture semantics explicit 와 같은 에러와 수정 제안이 뜬다... closure 와 이를 담고 있는 우리의 ViewModel(여기서 self.) 이 모두 Reference Type 이므로 strong reference cycle 을 방지하기 위해 명시적으로 self. 를 붙이면서 확인하라는 뜻이다...일단 backgroundImage 앞에 self. 을 붙이면 해결이 되는 듯 보이지만 실제로는 앞에서 말한 strong reference cycle 이 존재하는 상태다...

    reference type 의 경우 어떤 함수 / 클로저 / 상수 / 변수 중 하나라도 해당 reference type 을 담고 있다면 계속 메모리 상에 저장된다

  • 메인 큐는 인자로 클로저를 받는데 클로저는 reference type 이므로 let imageData = try? Data(contentsOf: url) 줄의 작업이 끝날때까지 메모리에 저장된다, 마찬가지로 우리의 ViewModelreference type 이므로 메모리에 저장되는데 self. 으로 인해 메모리 상에 있는 메인 큐의 인자 클로저가 우리의 ViewModel 을 가리키게 되어 strong reference 가 생기므로 파일을 닫더라도 힙에 ViewModel 이 남아있게 된다...즉, strong reference cycle 이 존재하는 상태이다

    클래스의 새로운 인스턴스를 생성할 때마다 ARC(Automatic Refernce Cycle) 는 해당 인스턴스를 저장하기 위한 메모리를 할당하며, 해당 메모리는 해당 인스턴스를 가리키는 프로퍼티 / 변수 / 상수가 하나라도 존지하는 한 프리되지 않는데 이를 strong reference 라고 한다. 어떤 strong reference 가 0이 되는 경우가 없을 때, 즉 언제나 하나 이상의 프로퍼티 / 변수 / 상수가 해당 인스턴스를 가리키는 경우 strong reference cycle 이 발생했다고 한다. 킹갓공식문서


그래서 해결은요

  • 클로저와 클래스 인스턴스 간의 strong reference cycle 은 클로저 정의 시 클로저가 reference type 을 내부에 캡쳐할 때 따르는 규칙을 담고 있는 capture list 를 함께 정의함으로써 해결한다!
  • 레퍼런스가 필요에 따라 weak 혹은 unowned 가 되도록 정의해주면 된다...!
    • weak : 인스턴스가 먼저 프리되어 nil이 될 때 사용
    • unowned : 클로저와 인스턴스가 항상 서로를 가리키고, 동시에 프리될 때 사용
  • 따라서 여기서는 weak 을 이용해서 해당 클로저 내부에서만 해당 변수를 재정의해서 이 문제를 해결할 수 있다...! weak 을 붙이면 옵셔널이 되어 다른 누구도 힙에 해당 변수를 보관하고 있지 않다면 더 이상 힙에 저장하지 않고 nil 로 바꿔준다...!
    private func fetchBackgroundImageDataIfNecessary() {
        backgroundImage = nil
        switch emojiArt.background {
        case .url(let url):
            // fetch the url
            DispatchQueue.global(qos: .userInitiated).async {
                let imageData = try? Data(contentsOf: url)
                DispatchQueue.main.async { [weak self] in  // weak 선언
                    if imageData != nil {
                        self?.backgroundImage = UIImage(data: imageData!)
                    }
                }
            }
        case .imageData(let data):
            backgroundImage = UIImage(data: data)
        case .blank:
            break
        }
    }

# 로딩이 오래 걸릴 때 뒷북 방지하는 법

  • 배경화면의 로딩이 오래 걸리는 경우 사용자가 로딩 중에 그냥 다른 배경을 드래그 & 드랍할 수 있다. 이때 사용자 입장에서 이미 버린 이전의 배경이 뒤늦게 로딩되는 것을 막으려면 지금 세계가 내가 어떤 이미지의 로딩을 요구했을 때의 세계와 같은 지 확인하고, 같을 때만 배경화면을 불러온 이미지로 교체해야 한다...! 작업을 요청 시의 세계와 수행 시의 세계가 같을 때만 작업을 수행하는 것이 **비동기 처리**의 핵심..!
  • if self?.emojiArt.background == EmojiArtModel.Background.url(url) 을 이용해서 Model 의 배경화면과 현재 불러온 url 의 배경화면이 일치하는 지 확인!
    private func fetchBackgroundImageDataIfNecessary() {
        backgroundImage = nil
        switch emojiArt.background {
        case .url(let url):
            // fetch the url
            DispatchQueue.global(qos: .userInitiated).async {
                let imageData = try? Data(contentsOf: url)
                DispatchQueue.main.async { [weak self] in  // weak 선언
                    if self?.emojiArt.background == EmojiArtModel.Background.url(url) {  // 환경이 같은 지 확인
                        if imageData != nil {
                            self?.backgroundImage = UIImage(data: imageData!)
                        }
                    }
                }
            }
            ....
    }

# View가 제스처를 인식하게 하기

  • .gesture view modifier 사용
myView.gesture(theGesture)
  • theGestureGesture 프로토콜 을 따르며 함수 / 연산 프로퍼티 / Viewbody var 내부의 지역변수 로 생성됨
var theGesture: some Gesture {
    return TapGesture(count: 2)
}

# 인식한 제스처에 반응하기

discrete gesture

  • 한 번에 일어남, 제스처가 시작과 동시에 끝나며 하나의 반응만을 필요로 함
  • .onEnded { } 사용
var theGesture: some Gesture {
    return TapGesture(count: 2)
        .onEnded { // do sth }
}
  • convenience versions 가 존재
myView.onTapGesture(count: Int) { /* do sth */ }

non-discrete gesture

  • 제스처가 발생하는 동안 여기에 대해 반응해야 함
    • e.g. DragGesture, Magnification Gesture, RotationGesture, LongPressGesture(i.e. fingers down and fingers up
  • .onEnded 를 사용하는 것은 같으나 아래 슬라이드와 같이 value 인자가 생김...! value 는 해당 제스처가 끝났을 때의 상태를 알려주며 제스처 종류에 따라 상이
    • e.g. DragGesture : 손가락의 시작 지점과 끝 지점 등을 담고 있는 구조체

  • 핵심은 value 에 따라 @GestureState 를 이용해서 제스처의 상태를 계속 업데이트함으로써 제스처 시행 도중에도 반응을 할 수 있음...! 단 제스처가 종료된 후에는 <starting value> 로 돌아감
@GestureState var myGestureState: MyGestureStateType = <starting value>

  • .updating 안에 @GestureState 프로퍼티 래퍼가 붙은 변수 앞에 $ 표시를 붙여서 사용...!

  • .onChanged 라는 단순한 버전이 있긴 한데 얘는 과정을 나타내지는 못하고, 제스처의 결과로 나타낼 반응이 손가락의 절대적인 위치와 관련이 있을 때(e.g. 유저가 손가락을 펜처럼 사용하는 경우) 유용하다. 그러나 상대적인 위치에 반응하는 경우(e.g. 드래그한 정도)에는 .updating() 이 적절하다...! 아래 슬라이드 참고..!


# 더블 클릭하면 화면 맞춤...! : discrete gesture

  • 더블 클릭했을 때 배경화면이 화면 크기에 딱 맞게 바뀌도록 하는 게 목표...!
  • 먼저 gesture 를 리턴하는 doubleTapToZoom() 함수 를 만들고, zoomToFit() 함수 를 만들어서 doubleTapToZoom() 함수 가 호출되었을 때 어떠한 동작을 하게 할 지 지정해줬다.

  • zoomToFit() 함수 에서 도출한 zoom 한 상태의 길이와 너비를 어떻게 배경이미지에 적용해야할 지 어려웠는데 @State var scale: CGFloat 을 선언하고 배경 이미지에 .scaleEffect(zoomScale) 로 적용한 다음 얘를 계속 업데이트 해주는 방식이었다...!

struct EmojiArDocumentView {
    var documentBody: some View {
        GeometryReader { geometry in
            ZStack {
                Color.white
                    .overlay(
                        OptionalImage(uiImage: document.backgroundImage)
                            .position(convertFromEmojiCoordinates((0, 0), in: geometry))
                            .scaleEffect(zoomScale)  // 여기 적용
                    )
                    .gesture(doubleTapToZoom(in: geometry.size))  // 제스처!
                ...
    }
    
    // MARK: - Zoom
    
    @State var zoomScale: CGFloat = 1

    private func doubleTapToZoom(in size: CGSize) -> some Gesture {
        TapGesture(count: 2)
            .onEnded {
                withAnimation {
                    zoomToFit(image: document.backgroundImage, in: size)
                }
            }
    }
    
    private func zoomToFit(image: UIImage?, in size: CGSize) {
        if let image = image, image.size.height > 0, image.size.width > 0,
            size.width > 0, size.height > 0  {
            let hZoom = size.width / image.size.width
            let vZoom = size.height / image.size.height
            
            zoomScale = min(hZoom, vZoom)
        }
    }
}

# Clipped

  • View 는 디폴트로 자신에게 주어진 영역 밖을 침범할 수 있으므로 .clipped() modifier 를 이용하면 해결된다...!

# Zoom Zoom Zoom...! : non-discrete gesture

  • 위의 더블 클릭 제스처와 달리 한번에 딱 끝나는 게 아니라 이번에는 두 손가락으로 zoom in/out 하는 동안 계속 zoom 정도 에 따라 이미지들의 크기가 업데이트 되도록 하는 제스처를 만들어야 한다.
  • 뭔가 제스처 중에 계속 업데이트 되어야 한다는 말 때문에 훨씬 어려울 것 같았는데 .updating 을 사용하면 인자로 가장 최근의 제스처 값이 알아서 들어와서 제스처 중에는 gestureZoomScale 을, 제스처가 끝났을 때는 steadyStateZoomScale 을 업데이트 해주면 됐다...!
    • 참고로 두 번째 인자는 실제로는 inout 매개변수 인데 일종의 syntatic sugar로 그냥 바꾸려고 하는 변수의 이름을 그대로 사용했다..!
    • 바인딩에 관해서는 다음 강의 때 다룬다고 하셔서 다음 번에 업데이트할 예정...!
  • 그리고 코드 수정을 줄이기 위해 교수님이 시키신 대로 zoomScale 를 연산 변수로 지정해줬다...! 이러면 제스처 중에는 gestureZoomScale 이 반영되고, 제스처가 끝나면 gestureZoomScale 은 다시 초기값인 1 로 돌아가 영향력이 없어지므로 steadyStateZoomScale 과 같아진다...!
  • 이렇게 제스처 함수를 구성하고 나서 zoom 이 영향을 줄 수 있는 곳들에 다 적절한 변수로 업데이트를 해줬다...! zoomToFit(), drop(), convertToEmojiCoordinates(), convertFromEmojiCoordinates(), documentBodyOptionalImageemoji.text
struct EmojiArtDocumentModel {
    ...
    @State private var steadyStateZoomScale: CGFloat = 1
    @GestureState private var gestureZoomScale: CGFloat = 1
    
    private var zoomScale: CGFloat {
        steadyStateZoomScale * gestureZoomScale
    }
    
    private func zoomGesture() -> some Gesture {
        MagnificationGesture()
            .updating($gestureZoomScale) { latestGestureScale, gestureZoomScale, _ in
                gestureZoomScale = latestGestureScale
            }
            .onEnded { gestureScaleAtEnd in
                steadyStateZoomScale *= gestureScaleAtEnd
            }
    }
    ...
}

View Modifier ORDERS MATTER!!!!!!!!!!

  • 문제는...zoom in/out 을 하고 나서 이모지를 추가하면 이상한 위치에 자꾸 추가됐다...뭔가 zoomScale 을 도출하는 부분이나 이를 사용해서 좌표를 구하는 부분에서 문제가 있나 싶어서 코드를 다 보고, demo code 랑도 비교를 해봤는데 문제가 없었다 결국 전체 코드를 다 뜯어보기 시작했는데...
  • 그렇다...멍청하게도 position() 다음에 scaleEffect() 를 적용하고 있었다...이모지부터 추가하고 그 상태에서 줌을 하니...당연히 이상하게 보였던 것...아래와 같이 바꿔줬더니 바로 멀쩡하게 원하는 위치에 이모지가 추가됐다...기쁜데...슬펐다...
struct EmojiArtDocumentView {
    ...
            ZStack {
                Color.white
                    .overlay(
                        OptionalImage(uiImage: document.backgroundImage)
                            .scaleEffect(zoomScale)
                            .position(convertFromEmojiCoordinates((0, 0), in: geometry))
                    )
                    .gesture(doubleTapToZoom(in: geometry.size))
                if document.backgroundImageFetchStatus == .fetching {
                    ProgressView()
                        .scaleEffect(2)
                } else {
                    ForEach(document.emojis) { emoji in
                        Text(emoji.text)
                            .font(.system(size: fontSize(for: emoji)))
                            .scaleEffect(zoomScale)  // 바로 여기...
                            .position(position(for: emoji, in: geometry))
                    }
                }
            }
    ....
}

# 그림 밀고 당기기~

  • panGesture() 구현의 경우 CGSize extension 을 몇 개 추가해야되는 것 빼고는 비교적 쉽게 구현할 수 있었다...해당 함수에서 offSet 의 정도를 zoomScale 로 나눠주는 것까지는 바로 이해가 갔는데(확대된 상태에서는 실제 손가락이 이동한 거리보다 이미지가 이동한 거리가 작을 것이므로)...
  • panOffset 변수 에서 교수님 설명에 의하면 확대된 상태에서는 더 많이 이동하고 축소된 상태에서는 덜 이동해야 하므로 zoomScale 을 곱해줘야 한다는데 이게 이해가 안갔다...위의 로직을 생각해 보면 그 반대가 아닌가...? 그래서 내 생각대로 나누기 처리를 해 봤는데 이러니까 예상한 것보다 너무 적게 움직였다...
struct EmojiArtDocumentView {
    ...
    @State private var steadyStatePanOffset: CGSize = CGSize.zero
    @GestureState private var gesturePanOffset: CGSize = CGSize.zero
    
    private var panOffset: CGSize {
        (steadyStatePanOffset + gesturePanOffset) * zoomScale
    }
    
    private func panGesture() -> some Gesture {
        DragGesture()
            .updating($gesturePanOffset) { latestDragGestureValue, gesturePanOffset, _ in
                gesturePanOffset = latestDragGestureValue.translation / zoomScale
            }
            .onEnded { finalDragGestureValue in
                steadyStatePanOffset =  steadyStatePanOffset + (finalDragGestureValue.translation / zoomScale)
            }
    }
    ...
}

private func convertToEmojiCoordinates(_ location: CGPoint, in geometry: GeometryProxy) -> (Int, Int) {
    let center = geometry.frame(in: .local).center
    let location = (
        x: (location.x - center.x - panOffset.width) / zoomScale,
        y: (location.y - center.y - panOffset.height) / zoomScale
    )
    return (Int(location.x), Int(location.y))
}
private func convertFromEmojiCoordinates(_ location: (x: Int, y: Int), in geometry: GeometryProxy) -> CGPoint {
    let center = geometry.frame(in: .local).center    
    return CGPoint(
        x: center.x + CGFloat(location.x) * zoomScale + panOffset.width,
        y: center.y + CGFloat(location.y) * zoomScale + panOffset.height
    )
}
  • 내가 일단 이해하고 넘어간 방식panOffset 변수 가 사용되는 미치는 두 함수 convertFromEmojiCoordinates()convertToEmojiCoordinates() 를 봤더니 함수 자체에서 이미 zoomScale 만큼 나눗셈과 곱셈을 하고 있어서 여기에 맞춰서 표현하기 위해 그렇게 하신 것 같다...약간 panOffset 변수 자체를 zoomScale 에 대한 상대값으로 생각하셔서 그런 것 같은데 나차럼 panOffset 변수 를 절댓값으로 생각했다면 아래와 같이 구성하는 게 더 이해가 쉬울 것 같다..!
import SwiftUI

struct EmojiArtDocumentView: View {
    ...
    private var panOffset: CGSize {
        (steadyStatePanOffset + gesturePanOffset)
    }
    
    private func convertToEmojiCoordinates(_ location: CGPoint, in geometry: GeometryProxy) -> (Int, Int) {
        let center = geometry.frame(in: .local).center
        let location = (
            x: (location.x - center.x) / zoomScale - panOffset.width,
            y: (location.y - center.y) / zoomScale - panOffset.height
        return (Int(location.x), Int(location.y))
    }
    
    private func convertFromEmojiCoordinates(_ location: (x: Int, y: Int), in geometry: GeometryProxy) -> CGPoint {
        let center = geometry.frame(in: .local).center
        
        return CGPoint(
            x: center.x + (CGFloat(location.x) + panOffset.width) * zoomScale,
            y: center.y + (CGFloat(location.y) + panOffset.height) * zoomScale
        )
    }
    ...
}

# 두 개의 제스처 동시 적용

  • .gesture(panGesture().simultaneously(with: zoomGesture())
  • 하나의 뷰에 두 개 이상의 제스처를 붙이면 의미가 명확하지 않으므로 지양해야 함

☀️ 느낀점

  • 아마도 최초로 삼수강한 강의...욕심이 생겨서 포스팅 쓰면서 공부도 가장 많이 했고 암튼 정말 오래걸렸다...물론 빡집중했다면 더 빨리 끝낼 수 있었겠지만 아무튼 결국 완결냈다는 것에 의의를 두는 것으로...ㅋㅋㅋ

  • 늘 느끼는 거지만 강의 들을 땐 참 쉽죠~? 인데 막상 내가 해보면 아니 여기서 어떻게 한거지?! 싶고 실수연발이라 혼자 뭐가 문제야 맞는데 왜 틀리대...하면서 시간을 엄청 날린다...어떻게 보면 당연한건데 그럴 때마다 조급해지는 게 문제...사실은 그냥 빨리 잘하고 싶은 내 마음이 문제같기도...ㅎㅎㅎ







how to load images and url

  • in the View
  • inside the drop function! switch using the found var...

actually fetching the background in the viewModel

  • backgroundImage

background queue can't be published u know!

  1. 응 안 기다려~ : background queue
  2. 크큭...약한 놈... : weak instead of .self
  • weak turn .self into an optional(turns nil if no one keeps it in the heap)
  1. 뒷북금지ㅋㅎ
    private func fetchBackgroundImageDataIfNecessary() {
        // why do I have to set this to nil...?
        backgroundImage = nil
        switch emojiArt.background {
        case .url(let url):
            // fetch the url
            DispatchQueue.global(qos: .userInitiated).async {
                let imageData = try? Data(contentsOf: url)
                DispatchQueue.main.async {
                    
                    if imageData != nil {
                        // u can never publish sth in a background thread!!
                        self.backgroundImage = UIImage(data: imageData!)
                    }
                }
            }
        case .imageData(let data):
            backgroundImage = UIImage(data: data)
        case .blank:
            break
        }

#### continued...getting rid of .self - weak self!

var theGuesture: some Gesture {}

  • all about changing ur @GestureState var based on the value repeatedly
  • need to think about what state i need to know about in order to draw my View

discrete and indiscrete gestures

  • have the lecture slide images...

Pinch (non-discrete)

  • need to update zoom while the pinch is actually happening...!
  • what state is changing while we're pinching?
  • in this case, it's the zoomScale modified by how much we've pinched by so far
  • scale of the fingers only while the pinch is happening : so kinda read-only
    @GestureState private var gestureZoomScale: CGFloat = 1
  • inout

좋은 웹페이지 즐겨찾기