이번 프로젝트의 결과물

 

ViewModel

먼저 @Published 를 사용하여 publisher를 만듭니다. publisher에서 발생하는 이벤트 즉 값의 변화를 .sink subscriber를 사용하여 값의 변화에 대응하도록 만든 Observable ViewModel 입니다. 

class SubscriberViewModel: ObservableObject {
    
    @Published var count: Int = 0

    var cancellables = Set<AnyCancellable>()
    
    @Published var textFieldText: String = ""

    @Published var textIsValid: Bool = false

    @Published var showButton: Bool = false
    
    init() {
        setUpTimer()
        addTextFieldSubscriber()
        addButtonSubscriber()
    }
    
    func setUpTimer() {
        Timer.publish(every: 1, on: .main, in: .common)
            .autoconnect()
            .sink { [weak self]  _ in
                guard let self = self else { return }
                print("시작하는데\(count)")
                self.count += 1

            }
            .store(in: &cancellables)
    }
    
    func addButtonSubscriber() {
        $textIsValid
            .combineLatest($count)
            .sink { [weak self] (isValid, count) in
                guard let self = self else { return }
                if isValid && count >= 10 {
                    self.showButton = true
                } else {
                    self.showButton = false
                }
            }
            .store(in: &cancellables)
    }
    
    func addTextFieldSubscriber() {
        $textFieldText
            .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
            .map { text -> Bool in
                if text.count > 3 {
                    return true
                }
                return false
            }
            .sink(receiveValue: { [weak self] isValid in
                self?.textIsValid = isValid
            })
            .store(in: &cancellables)
    }
    
}

 

ViewModel의 구체적인 설명

count는 1씩 증가하는 값을 저장하는 @Publish 프로퍼티로 $를 붙여 publisher로 사용될 수 있습니다, 이후에 선언된 textFieldText, textIsValid, showButton 같은 경우도 마찬가지입니다.

@Published var count: Int = 0

 

Set<AnyCancellable> 타입인 cancellables 여러개의 publisher를 담을 수 있으며 AnyCancellable는 Combine 라이브러리에서 제공하는 타입입니다

var cancellables = Set<AnyCancellable>()

AnyCancellable이란?

간단히 말해 publisher의 publish를 취소할 수 있도록 해주는 Combine 라이브러리에서 제공하는 타입입니다.

 

publisher 로 타이머 동작시키기

setUpTimer 함수는 Timer를 publish하여 1초마다 이벤트를 발생시키도록 합니다. 이때 .sink를 사용하여 변화, 이벤트가 발생할 때마다 대응 합니다. 아래에서는 count를 1씩 증가시킵니다.

func setUpTimer() {
    Timer.publish(every: 1, on: .main, in: .common)
        .autoconnect()
        .sink { [weak self]  _ in
            guard let self = self else { return }
            self.count += 1
        }
        .store(in: &cancellables)
}

 

TextField 변화를 publisher 를 이용하여 대응하기 1

$textIsValid에서 $를 붙이므로 textIsValid는 publisher가 됩니다. 이때 publisher에서 .combineLastest()를 사용하여 인자값으로 받는 count publisher에서 가장 마지막으로 publish된 값과 함께 TextField의 변화에 동시에 대응합니다.

func addButtonSubscriber() {
    $textIsValid
        .combineLatest($count)
        .sink { [weak self] (isValid, count) in
            guard let self = self else { return }
            if isValid && count >= 10 {
                self.showButton = true
            } else {
                self.showButton = false
            }
        }
        .store(in: &cancellables)
}

 

TextField 변화를 publisher 를 이용하여 대응하기 2

$textFieldText에서 $를 붙이므로 textFieldText는 publisher가 됩니다. 이때 .debouncd(for: _, scheduler: _)를 사용하였는데 이것은 publish에 대한 대응을 0.5초 느리게 한다는 것을 의미하며 map에 로직 또한 값이 실제 변화한지 0.5초 뒤에 계산됩니다.

func addTextFieldSubscriber() {
    $textFieldText
        .debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)
        .map { text -> Bool in
            if text.count > 3 {
                return true
            }
            return false
        }
        .sink(receiveValue: { [weak self] isValid in
            self?.textIsValid = isValid
        })
        .store(in: &cancellables)
}

 

 

View 총 코드

struct ContentView: View {
    
    @StateObject var vm = SubscriberViewModel()
    
    var body: some View {
        VStack {
            Text("\(vm.count)")
            
            Text("\(vm.textIsValid)")

            TextField("입력", text: $vm.textFieldText)
                .padding(.leading)
                .frame(height: 55)
                .font(.headline)
                .background(Color.secondary)
                .clipShape(RoundedRectangle(cornerRadius: 10))
                .overlay(
                    ZStack {
                        Image(systemName: "xmark")
                            .foregroundStyle(.red)
                            .opacity(
                                vm.textFieldText.count < 1 ? 0.0 :
                                vm.textIsValid ? 0.0 : 1.0)
                        
                        Image(systemName: "checkmark")
                            .foregroundStyle(.green)
                            .opacity(vm.textIsValid ? 1.0 : 0.0)
                    }
                        .font(.headline)
                        .padding(.trailing)
                        
                    , alignment: .trailing
                )
            
            Button(action: {
                
            }, label: {
                Text("제출")
                    .font(.headline)
                    .foregroundStyle(.white)
                    .frame(height: 55)
                    .frame(maxWidth: .infinity)
                    .background(.blue)
                    .clipShape(RoundedRectangle(cornerRadius: 20))
                    .opacity(vm.showButton ? 1.0 : 0.5)
            })
            .disabled(!vm.showButton)
        }
        .padding()
    }
}

 

'SwiftUI' 카테고리의 다른 글

haptics / vibration (진동)  (0) 2024.04.12
Publishers and Subscribers (Combine)  (0) 2024.04.11
Starting Vapor (Swift로 서버 만들기)  (0) 2024.04.09
타이머를 이용하여 아두이노 LED 켜기  (0) 2024.04.09
Timer 타이머  (0) 2024.04.08
ytw_developer