개발을 시작하는 이야기

[SwiftUI]Lecture 9: EmojiArt Drag and Drop Multithreading 본문

개발 이야기/Swift

[SwiftUI]Lecture 9: EmojiArt Drag and Drop Multithreading

Teiresias 2022. 4. 23. 22:00

강의 보기 : YouTube :: Stanford

 

Collections of Identifiable

Identifiable

A class of types whose instances hold the value of an entity with stable identity.
인스턴스가 안정적으로 구분될 수 있는 엔티티 값을 가지도록 하는 타입의 클래스
func choose(_ card: Card) {
	if let index = cards.firstIndex(where: { $0.id == card.id }) {
    	cards[index].isFaceUp = true
    }
}

Color vs UIColor vs CGColor

Color

color-specifier : foregroundColor(Color.green)

ShapeStyle : fill(Color.blue)

Voew : Color.white

UIColor

다양한 색상 사용 가능

Color(uiColor: )

CGColor

Core Graphics 에서 사용 (color.cgColor is optional)

 

Color를 UIColor로 변경하기 위해서는 Color -> CGColor -> UIColor로 변경

Image vs UIImage

Image

이미지를 UI 상에 그려내는 View의 역할을 주로 한다.

Image(_ name: String) , Image(systemName:  )

UIImage

이미지 데이터를 관리하는 개체

Image(uiImage: )

 

원하는 UIImage를 UI상에 나타낼 때 Image를 사용하면 된다.

Drag and Drop

Item Provider

NSItemProvider

드래그 앤 드랍과 같은 과정을 통해 서로 다른 프로세스 간 비동기적 데이터 또는 파일을 전달하기 위한 항목을 제공한다.

Multithreaded Programming

UI는 항상 사용자의 모션에 반응할 준비가 되어 있어야 하기 때문에 시간이 오래 걸리는 작업이 필요한 경우 UI가 실행되고 있는 메인 스레드가 아닌 백그라운드 스레드에서 비동기로 처리하도록 한다. 스위프트는 큐를 이용해서 멀티스레딩을 하는데, 작업 내용이 될 코드 블록을 큐에 넣으면 시스템이 이러한 코드들을 큐에 들어있는 순서대로 하나씩 적절한 스레드에 분배하게 된다. 

 

Main Queue는 UI에 영향을 미칠 수도 있는 모든 코드 블록을 담고 있다. 시스템은 메인 큐의 코드 블록을 실행하기 위해서 단일 스레드를 사용한다. 자료구조에 쓰기 작업은 메인 큐에서, 읽기 작업은 백그라운드 큐에서 수행함으로써 동기화가 가능하다.


Drag and Drop 을 활용한 새로운 프로젝트를 실행한다.

struct EmojiArtModel {
    
    struct Emoji: Identifiable, Hashable {
        let text: String
        var x: Int // offset from the center
        var y: Int // offset from the center
        var size: Int
        var id: Int
        
        //private를 선언하기 위해서 작성함
        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
        }
    }
    
    init() { }
    
}

 모델의 경우 사용자가 크기와 위치를 변경할 수 있어야 하기 때문에 private로 선언할 수 없다. 하지만 모델 밖에서 free init을 통해 임의로 이모지를 생성하는 것을 막고 싶으므로 filepricate init으로 선언해 모델 밖에서 임의로 이모지를 생성하는 것을 막았다.

 마찬가지로 EmojiModel의 free init을 외부에서 임의로 사용하는 것을 방지하기 위해 빈 init을 설정해준다. 

 

extension EmojiArtModel {
    
    enum Background: Equatable {
        case blank
        case url(URL)
        case imageData(Data)
        
        var url: URL? {
            switch self {
            case .url(let url): return url
            default: return nil
            }
        }
        
        var imageData: Data? {
            switch self{
            case .imageData(let data): return data
            default: return nil
            }
        }
    }
}

imagedata와 url변수를 통해 background 구조체의 인스턴스가 이미지 데이터 혹은 url을 갖고 있는지 확인할 수 있다.

 

struct ScrollingEmojisView: View {
    let emojis: String
    
    var body: some View {
        ScrollView(.horizontal) {
            HStack {
                ForEach(emojis.map { String($0) }, id: \.self) { emoji in
                    Text(emoji)
                        .onDrag { NSItemProvider(object: emoji as NSString) }
                }
            }
        }
    }
}

ScrollingEmojisView에서 임의의 이모지를 drag 해서 background에 보내고 싶은 것이기 때문에 NSItemProvider가 사용될 수 있도록 emoji를 NSString으로 변환해서 보내준다.

 

Drop emoji의 핵심은 데모코드인데 별도로 받아야 한다

[데모 코드는 이곳에서]

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

NSItemProvider의 object를 @escaping 클로저를 이용해서 처리하고 Model의 emoji 배열에 추가하여 드롭할 수 있게 해 주어서 성공 여부를 반환한다.


이전 예제에 익숙해졌더니 새로운 Drag and Drop의 시작. 쉽지 않네...

멀티스레딩에 대해서도 좀 더 열심히 공부해서 별도의 포스팅을 작성해야겠다.