import SwiftUI
struct MeetingView: View {
@Binding var scrum: DailyScrum
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 16.0)
.fill(scrum.theme.mainColor)
VStack {
ProgressView(value: 5, total: 15)
HStack {
VStack(alignment: .leading) {
Text("Seconds Elapsed")
.font(.caption)
Label("300", systemImage: "hourglass.tophalf.fill")
}
Spacer()
VStack(alignment: .trailing) {
Text("Seconds Remaining")
.font(.caption)
Label("600", systemImage: "hourglass.bottomhalf.fill")
}
}
.accessibilityElement(children: .ignore)
.accessibilityLabel("Time remaining")
.accessibilityValue("10 minutes")
Circle()
.strokeBorder(lineWidth: 24)
HStack {
Text("Speaker 1 of 3")
Spacer()
Button(action: {}) {
Image(systemName: "forward.fill")
}
.accessibilityLabel("Next speaker")
}
}
}
.padding()
.foregroundColor(scrum.theme.accentColor)
.navigationBarTitleDisplayMode(.inline)
}
}
#Preview {
@Previewable @State var scrum = DailyScrum.sampleData[0]
MeetingView(scrum: $scrum)
}
위에 있는 MeetingView 를 조각조각 기능에 맞게 쪼개야한다.
오늘 온라인 세션 SwiftUI 를 들으면서 instrument 를 사용하여 성능 측정하는 것을 봤는데,
뷰의 값이 변경될 때마다 계속 갱신되기 때문에, 각 뷰를 따로 만들어서 관리하는 것이 성능 상 좋다는 것을 시각적으로 알게된 것 같다.
단일 뷰보다는 각각의 뷰에 Binding 시킨 후, 값의 변화에 따라 조각난 뷰만 갱신시키는 것이 좋은 방법이라는 것이 크게 와닿았다.
일단~~!! 쪼개보자
MeetingHeaderView
- private computed property 연산 코드를 분리한다
totalSeconds 전체 시간 계산
progress
minutesRemaining : 초를 분으로 계산
커스텀하게 HeaderView 를 만들기 위해
세가지 값을 받도록 View 를 구성한다.
import SwiftUI
import ThemeKit
struct MeetingHeaderView: View {
let secondsElapsed: Int
let secondsRemaining: Int
let theme: Theme
private var totalSeconds: Int {
secondsElapsed + secondsRemaining
}
private var progress: Double {
guard totalSeconds > 0 else { return 1 }
return Double (secondsElapsed) / Double(totalSeconds)
}
private var minutesRemaining: Int {
// 초 -> 분
secondsRemaining / 60
}
var body: some View {
ProgressView(value: progress)
// 아래의 ScrumProgressViewStyle 은 만들어야함***
// .progressViewStyle(ScrumProgressViewStyle(theme: theme)))
HStack {
VStack(alignment: .leading) {
Text("Seconds Elapsed")
.font(.caption)
Label("300", systemImage: "hourglass.tophalf.fill")
}
Spacer()
VStack(alignment: .trailing) {
Text("Seconds Remaining")
.font(.caption)
Label("600", systemImage: "hourglass.bottomhalf.fill")
.labelStyle(.trailingIcon) // text > 아이콘 순으로 변경
}
}
// 접근성 적용
.accessibilityElement(children: .ignore)
.accessibilityLabel("Time remaining")
.accessibilityValue("10 minute")
.padding([.top, .horizontal])
}
}
#Preview(traits: .sizeThatFitsLayout) {
MeetingHeaderView(secondsElapsed: 60, secondsRemaining: 30, theme: .bubblegum)
}
TimerKit -> ScrumTimer
apple 에서는 TimerKit 을 제공해주고 있었다.
이 구조에 대해서도 좀더 분해해서 공부해볼 예정.
저런 식으로 하면 여기저기 타이머 관련 개발 시에 차용해서 쓰면 좋을 거 같다는 생각이 들었다.
import TimerKit //추가
.progressViewStyle(ScrumProgressViewStyle(theme: theme)) // 주석 제거
ScrumTimer 파일은 아래와 같은 구조로 만들어져있다.
// Speaker (발표자) 의 구조체가 있다.
// 여기서 isCompleted 로 발표시간이 종료되었는지를 감지하며, Identifiable 하다.
public var activeSpeaker (isCompleted 가 true 인 발표자)
public var secondsElapsed : 경과된 시간(초)
public var secondsRemaining : 남은 시간(초)
private var _speakers: [Speaker] = [] //발표할 사람들의 배열
public var speakers: [Speaker] {
_speakers // public 하게 해서 외부에서 접근할 수 있ㄷ록
}
public var speakerChangedAction: (() -> Void)? //발표자 변경 시 = 새로운 발표자가 나타날 경우 실행되는 closure
// ScrumTimer 에서 처리되는 요소들
private var lengthInMinutes: Int
private weak var timer: Timer?
private var timerStopped = false
private var frequency: TimeInterval { 1.0 / 60.0 }
private var lengthInSeconds: Int { lengthInMinutes * 60 }
// 남은시간 퍼센트 표시
private var secondsPerSpeaker: Int {
(lengthInMinutes * 60) / _speakers.count
}
// 해당 발표자가 경과한 시간 표시
private var secondsElapsedForSpeaker: Int = 0
// speakers 의 값에 따라 적용되는 요소들
private var speakerIndex: Int = 0
private var speakerText: String {
return "Speaker \(speakerIndex + 1): " + _speakers[speakerIndex].name
}
private var startDate: Date?
함수들 살펴보기!!!
init() < Timer 의 초기화
startScrum
stopScrum
skipSpeaker 를 보고 있는데, 조금 낯선 단어의 등장.
// nonisolated 가 뭐지??
nonisolated public func skipSpeaker() {
Task { @MainActor in
changeToSpeaker(at: speakerIndex + 1)
}
}
Swift 에서 actor, isolated, non isolated
Actor, isolated, nonisolated 키워드는 동시성(Concurrency) 관련한 개념과 연관이 있다.
Actor 모델을 활용하여 데이터를 보호하도록 한다.
Actor = 멀티스레드 환경에서 안전하게 공유 데이터를 보호하는 특수한 타입을 말한다.
Actor 내부의 속성과 메소드는 기본적으로 Serial화 되어 실행된다
이 내부 속성에 접근하려면 await 키워드를 사용해야한다.
Await 키워드를 활용하면 actor 내부의 속성 및 메소드에 접근할 수 있다.
Isolated = 특정 매개변수가 Actor의 인스턴스를 의미할 때 사용한다. Isolated 를 사용하면 해당 actor 내부에서 실행할 때 awaait 없이 접근이 가능하다.
Actor 안에서 실행할 경우에는 별다른 키워드 추가 없이 내부에서 await 없이 사용하면 된다.
이 키워드도 일단은 ~~~~~ 다음번에 좀 더 정리하는 걸로ㅠㅠㅠㅠ 점점 길어지는 군
reset
changeToSpeaker
update
generateSpeakersList // name list 를 이용해 Speaker 구조체 배열을 만듦
private static func generateSpeakersList(with names: [String]) -> [Speaker] {}
// 여기서 with 는 함수의 매개변수 레이블(Parameter Label)이다.
// 여기선 person 이 매개변수이다. 함수 호출 시 더 자연스럽고 읽기 쉬운 코드를 만들기 위해 사용한다.
func greet(person name: String) {
print("Hello, \(name)!")
}
greet(person: "Taeni") // "Hello, Taeni!"
//보통 우리가 자주 봤던 코드 모양은 이런거였겠지만,이는 생략한 의미이다.
func greet(_ name: String) {}
그리고 generateSpeakersList 에는 guard 키워드가 있는데,
조건을 만족하지 않으면 즉시 함수나 코드 블록을 빠져나가도록 하는 키워드이다.
if 해서 else 일 경우 return 시키거나 하는 식으로 쓰는데, 그를 더 깔끔하게 작성하기 위한 코드이다.
다시 한번 더 찾아봤다.
guard 조건을 만족하지 않으면 코드 블록 종료, 옵셔널 바인딩과 함께 사용하고 루프에서 continue가 가능함. 조기종료가 가능함
return 현재 함수 실행 중단 후 반환
break switch 또는 loop 종료
throw 예외(Error) 발생
continue 루프의 현재 반복 건너뛰기
import SwiftUI
import TimerKit
struct MeetingView: View {
@Binding var scrum: DailyScrum
@State var scrumTimer = ScrumTimer()
var body: some View {
ZStack{
RoundedRectangle(cornerRadius: 16.0)
.fill(scrum.theme.mainColor)
VStack{
// 요 아래의 애들을 모두 MeetingHeaderView 로 바꿔치기할 것이다***
// ProgressView(value: 5, total: 10)
// HStack {
// VStack(alignment: .leading) {
// Text("Seconds Elapsed")
// .font(.caption)
// Label("300", systemImage: "hourglass.tophalf.fill")
// }
// Spacer()
// VStack(alignment: .trailing) {
// Text("Seconds Remaining")
// .font(.caption)
// Label("600", systemImage: "hourglass.bottomhalf.fill")
// }
// }
// // 접근성
// .accessibilityElement(children: .ignore)
// .accessibilityLabel("Time remaining")
// .accessibilityValue("10 minutes")
Circle()
.strokeBorder(lineWidth: 24)
HStack {
Text("Speaker 1 of 3")
Spacer()
Button(action: {}) {
Image(systemName: "forward.fill")
}
.accessibilityLabel("Next speaker")
}
}
}
.padding()
.foregroundColor(scrum.theme.accentColor)
.navigationBarTitleDisplayMode(.inline)//Configures the title display mode for this view.
}
}
#Preview {
@Previewable @State var scrum = DailyScrum.sampleData[1]
MeetingView(scrum: $scrum)
}
Add life cycle events
onAppear 과 onDisappear 을 이용해서 뷰가 나타나고 사라질 때의 이벤트에 맞게 life cycle method 를 사용할 수 있다.
struct MeetingView: View {
// 중간생략 ~_~
ZStack {
//...어쩌구저쩌구
}
//...어쩌구저쩌구
.onAppear {
// scrumTimer 를 reset 시킨 뒤, 시작한다
// attendees 들의 name 을 맵핑하여 리스트로 넘긴다.
scrumTimer.reset(lengthInMinutes: scrum.lengthInMinutes, attendeeNames: scrum.attendees.map { $0.name })
scrumTimer.startScrum()
// scrumTimer 공부 좀 더 해보자
}
.onDisappear{
scrumTimer.stopScrum()
// view 가 사라지면 멈춘다.
}
// 중간생략 ~_~
}
Extract the meeting footer
footer 도 분리해보자.
일단 있던 기존 코드를 뜯어냈다.
import SwiftUI
struct MeetingFooterView: View {
var body: some View {
HStack {
Text("Speaker 1 of 3")
Spacer()
Button(action: {}) {
Image(systemName: "forward.fill")
}
.accessibilityLabel("Next speaker")
}
}
}
#Preview(traits: .sizeThatFitsLayout) {
MeetingFooterView()
}
import SwiftUI
import TimerKit
struct MeetingFooterView: View {
let speakers: [ScrumTimer.Speaker]
var skipAction: () -> Void
private var speakerNumber: Int? {
guard let index = speakers.firstIndex(where: { !$0.isCompleted }) else { return nil }
return index + 1
}
private var isLastSpeaker: Bool {
//func dropLast(_ k: Int = 1) -> ArraySlice<Element>
//배열(Array) 또는 문자열(String)에서 마지막 n개의 요소를 제거한 새로운 시퀀스를 반환하는 메서드
return speakers.dropLast().allSatisfy { $0.isCompleted }
}
private var speakerText: String {
guard let speakerNumber = speakerNumber else { return "No more speakers" }
return "Speaker \(speakerNumber) of \(speakers.count)"
}
var body: some View {
VStack {
HStack {
if isLastSpeaker {
Text("Last Speaker")
}else {
Text(speakerText)
Spacer()
Button(action: skipAction) {
Image(systemName: "forward.fill")
}
.accessibilityLabel("Next speaker")
}
}
}
.padding([.bottom, .horizontal])
}
}
#Preview(traits: .sizeThatFitsLayout) {
@Previewable var speakers = DailyScrum.sampleData[0].attendees
.map { $0.name }
.map { ScrumTimer.Speaker(name: $0, isCompleted: false) }
MeetingFooterView(speakers: speakers, skipAction: {})
}
let nums = [1, 2, 3, 4, 5]
let dropped = nums.dropLast(2) // [1, 2, 3]
let suffix = nums.suffix(3) // [3, 4, 5]
let empty: [Int] = []
let result = empty.dropLast(2)
print(result) // []
빈 배열에 dropLast(_:)를 적용하면 그냥 빈 배열 반환한다 (에러안남)
// 예시!!
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// 모든 요소가 0보다 큰가?
let allPositive = numbers.allSatisfy { $0 > 0 }
print(allPositive) // true
// 하나라도 5 이상인 요소가 있는가?
let hasLargeNumber = numbers.contains { $0 >= 5 }
print(hasLargeNumber) // true
// 짝수만 필터링하기
let evenNumbers = numbers.filter { $0 % 2 == 0 }
print(evenNumbers) // [2, 4, 6, 8, 10]
// 첫 번째 짝수 찾기
let firstEven = numbers.first { $0 % 2 == 0 }
print(firstEven) // Optional(2)
// 특정 조건을 만족하는 동안 요소 버리고 남은 요소 반환
let droppedNumbers = numbers.drop(while: { $0 < 5 })
print(droppedNumbers) // [5, 6, 7, 8, 9, 10]
Trigger sound with AVFoundation
import AVFoundation
// ~생략~
private let player = AVPlayer.dingPlayer()
// ~생략~
.onAppear {
scrumTimer.speakerChangedAction = {
player.seek(to: .zero) // 처음으로 돌려서
player.play() // 딩 소리가 나게 한다.
}
}
'iOS > App' 카테고리의 다른 글
Scrumdinger 개발 07 : Persisting data (0) | 2025.04.07 |
---|---|
Scrumdinger 개발 06 : Updating app data (0) | 2025.04.06 |
Scrumdinger 개발 04 - Passing data with bindings (0) | 2025.04.03 |
Scrumdinger 개발 03 - Managing data flow between views ~ Creating the edit view (0) | 2025.04.02 |
Scrumdinger 개발 02 - Creating a navigation hierarchy (0) | 2025.04.01 |