이번 프로젝트의 결과물
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 |