[Mandarin Timer]

Woozoo·2023년 2월 25일
0

개인프로젝트

목록 보기
7/12

탭뷰가 있다면 각각의 뷰들을 네비게이션 뷰로 따로 감싸줘야한다!

네비게이션 뷰 안에 탭뷰를 감싸서 코드를 작성하고 있었는데
타이틀이 나오지 않는 다는 걸 알게됨

탭뷰는 각각 새로운 뷰이기 때문에 NavigationView도 각각 넣어줘야함!!


다시 시작함

모델에서 Date 값을 저장하는 게 아니라
String으로 변환된 Date값을 저장하고 이걸 왔다갔다 dateformatter를 사용해서 왔다갔다 하게끔 구성을 해줬다

Storage는 FileManager 사용했고!

이 글도 지금 두번째 쓰는중...
저장을 안해버렸다 ㅠ

현재까지 타이머 뷰 구현 완료했고, 타임 리스트 구현도 성공!!
그리고 각각의 Item들을 클릭하면 Navigation을 타고 들어가서 아이템에 대한 정보들도 표시해줬다!
또 데이터 persist까지 완료한 상태다

🥹 한 가지 아쉬운 점
타이머 저장하는 게 공부한 시점마다 다 개별적으로 나오는 데
같은 일자(day) 라면 하나로 합쳐서 보여주고, 터치해서 들어가면 그날의 공부 시간들을 보여주고 싶음

위에꺼 구현 못 해도 해야될 것들

  1. List에 아무것도 없다면 띄워줄 뷰!
  2. 전체적인 디자인
  3. 인포? 튜토리얼? Settings View
  4. 디자인이 아마 시간에 따른 귤 형상을 띄워주도록 enum 타입으로 만들어줘야할 거 같은데 이거 model에 추가해주는거!

현재 전체적으로 저장 되고 있는 게 [TimeModel] 리스트고,
이걸 년도+월 키워드로 grouping해서 [년도월 key : [TimeModel]]로 섹션에 뿌려주고 있음

23년 3월
16일 20초, 18일 30초 ,19일 20초, 19일 40초, 19일 1분

Array 메소드들 개념 다시 잡고 시작하자


Filter, Map, Reduce, CompactMap, FlatMap

https://www.youtube.com/watch?v=-mx_Kf3qKJY

struct IndieApp {
    let name: String
    let monthlyPrice: Double
    let users: Int
}

let appPortfolio: [IndieApp] = [
    IndieApp(name: "Creator View", monthlyPrice: 11.99, users: 4356),
    IndieApp(name: "FitHero", monthlyPrice: 0.00, users: 1756),
    IndieApp(name: "Buckets", monthlyPrice: 3.99, users: 7598),
    IndieApp(name: "Connect Four", monthlyPrice: 1.99, users: 34081),
]

// Filter

let freeApps = appPortfolio.filter { $0.monthlyPrice == 0 }
let highUsers = appPortfolio.filter { $0.users > 5000 }
print(freeApps)
print(highUsers)

filter는 조건 만족하는 애들만 걸러줌


HighUsers는 요런 뉘앙스겠죠

map은 다른 새로운 Array로 바꿔주는 메소드


appNames는 String 배열로 바꼈다


요런식으로!

reduce는 초기값 0 넣고 연산자로 합칠 수 있게 됨


요렇게 $0 $1의 users를 받아서 합칠 수 도 있겠고

여러개를 Chaining해서 써줄 수도 있겠다!!


요렇게 nil만 빼서 새로 배열 만들어주는 compactMap 메소드도 있고


어레이의 어레이를 하나로 다 합쳐주는 것도 있음
flatMap이 이런 메소드


여기에 또 map을 써서 바꿔주는 것도 가능합니다~!


오케이 다시 해봅시다

자 섹션이 나뉜 거 까지는 오케이.
나뉜 거에다가 만약에 TimeModel의 monthlyIdentifier가 같다면 TimeModel을 합쳐주는 메소드를 만들어야겠죠

방법 1번!

.reduce를 쓰는 거!!

func getSectionTimeData(key: String) -> [TimeModel] {
    let items = sectionTimeDic[key] ?? []
    let mergedItems = items.reduce(into: [String: TimeModel]()) { result, timeModel in
        let dailyIdentifier = timeModel.dailyIdentifier
        // if the result dictionary already contains a TimeModel with the same dailyIdentifier,
        // update its studySeconds by adding the current timeModel's studySeconds

        if var existingModel = result[dailyIdentifier] {
            existingModel.studySeconds += timeModel.studySeconds
            existingModel.breakSeconds += timeModel.breakSeconds
            result[dailyIdentifier] = existingModel
        } else {
            // otherwise, add the current timeModel to the result dictionary
            result[dailyIdentifier] = timeModel
        }
    }.values.sorted { $0.fullDate < $1.fullDate}
    // mergedTimeModels will contain an array of TimeModels with unique dailyIdentifiers
    // and their studySeconds merged if they have the same dailyIdentifier
    return mergedItems
}

방법 2번!

func getSectionTimeData(key: String) -> [TimeModel] {
    let items = sectionTimeDic[key] ?? []
    let mergedItems = Dictionary(grouping: items) { $0.dailyIdentifier }
        .map { (_, models) in
            models.reduce(TimeModel(fullDate: "", studySeconds: 0, breakSeconds: 0)) { result, model in
                TimeModel(fullDate: model.fullDate,
                          studySeconds: result.studySeconds + model.studySeconds,
                          breakSeconds: result.breakSeconds + model.breakSeconds)
            }
        }.sorted { $0.fullDate < $1.fullDate }
    return mergedItems
}

grouping을 dailyIdentifier로 다시해주고 reduce하는 거!!
2번으로 갑시다~


허허엇 버그를 발견했다🐜

타이머가 실행중일 때 타임리스트에서 아이템 선택해서 안으로 들어가면
네비게이션이 바로 닫히는 현상 발생

Timer가 업데이트 될 때마다 NavigationView가 닫힘

원래쓰던 getSectionTimeData로 할 때는 괜찮은데
merge 하는 과정에서 뭔가가 있나봄

2번으로 쓰던 방법 1번으로 바꾸니까 괜찮아졌다!!

그리고 타임리스트 day로 합친게 업데이트가 안되고 있음
합친 모델들도 삭제 안되고 있고

결국에는 이거 TimeModel 합칠라면
TimerListView에서 뿌릴 때 day로 나뉜 것도 [TimeModel]의 형태를 가지고 있어야하겠네

잠깐 그전에 타임셀에서
@State로 TimeModel을 받고 있었는데 이게 잘못됬었음
let 으로 해줘야함!!


어쩐지 업데이트가 될때도 있고 안될 때도 있더라..

저장하는 로직에서도 get으로 fetch해서 리로드하는것도 지워줌
이제 잘되네


자자 타임디테일뷰에서 받은 타임의 monthlyIdentifier를
활용해서 TimeList 필터를 거친 담에
해당 [TimeModel]을 이용해서 List 형태로 뿌려주면 되겠다

후우우...
긴 여정이었다

NavigationLink를 쓰게 되면 List에서 첨 그려질 때 로드가 되서
어떻게 하면 필터링 된 데이터들을 전달할 지에서 한참을 막혔음

결국 구현한 방법은 Detail에 dayKey 구멍을 뚫고

onAppear될 때 vm의 메소드가 실행되게 한 다음


vm의 dayTimeList를 가지고 리스트가 그려지게 해줬다


Delete 다시 구현하기

특정한 조건을 만족하는 아이템들을 array에서 삭제하는 건
.removeAll을 사용해서 가능함

dailyIdentifier의 키가 동일한 애들은 다 삭제되게 해줬다!

근데...
이러면 다 삭제되잖음

List로 보여지고 있는 개별적인 elements도 삭제가 가능하게 해줘야겠지

.onDelete로 가능한데 지금 기존의 timeList에도 접근해서 같이 삭제를 해줘야 하잖음?
어떻게 해줘야할까?

vm에서 offSets를 넘겨받아서 timeList에서 dayTimeList의 index에 해당하는 아이템의 id와 timeList의 id가 동일한 애를 찾고 remove를 이용해서 삭제, 그리고 dayTimeList에도 마찬가지로 삭제해줬다!!

마지막으로 해줄건 Delete 버튼을 눌렀을 때 alert이 뜨게 해주는 거!

참 alert이 deprecated 되서 confirmationDialog로 구현해줌!


타이머 뷰 디자인

로티 SPM

Lottie 써서 애니메이션 추가해야할 거 같음
로티 깃

로티가 UIKit으로 구현이 가능하지만 UIViewRepresentable을 사용해서 SwiftUI에서도 사용가능하게 해주자
SPM으로 설치해주고 새로운 LottieView라는 swift파일을 만들어줌


그리고 UIViewRepresentable을 채택해주면 경고가 나오는데
typealias로 UIViewType을 LottieAnimationView로 한 다음에 fix 버튼 눌러주자


그럼 LottieAnimationView에 맞는 메소드들이 구성됨
typealias는 다시 지워주고!!

JSON 로티파일 임포트 한 다음에

import SwiftUI
import Lottie

struct LottieView: UIViewRepresentable {
    func makeUIView(context: Context) -> Lottie.LottieAnimationView {
        let animationView = LottieAnimationView(name: "LottieProgressView")
        animationView.play()
        return animationView
    }
    
    func updateUIView(_ uiView: Lottie.LottieAnimationView, context: Context) {
        
    }
}

임포트한 JSON파일 이름을 LottieAnimationView의 이름으로 넣어주면 됨


그리고 사용하고자 하는 곳에서 방금 만들어준 LottieView를 불러와주면 5초정도 animation이 되는 걸 볼 수 있습니다~!

다시 로티뷰로 돌아와서 재사용 가능하게 바꿔줍시다

name이랑 loopMode 밖으로 빼줫으~!
근데 스케일 크기 안바껴서 아래처럼 LottieView 바꿔줌

struct LottieView: UIViewRepresentable {
    typealias UIViewType = UIView
    
    let name: String
    let loopMode: LottieLoopMode
    
    func makeUIView(context: UIViewRepresentableContext<LottieView>) -> UIView {
        let view = UIView(frame: .zero)
        
        //Add Animation
        let animationView = LottieAnimationView(name: name)
        animationView.loopMode = loopMode
        animationView.play()
        view.addSubview(animationView)
        
        animationView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            animationView.widthAnchor.constraint(equalTo: view.widthAnchor),
            animationView.heightAnchor.constraint(equalTo: view.heightAnchor)
        ])
        
        return view
    }
    
    func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<LottieView>) {
        
    }
}

로티 루프한 거 stop하는 기능 못찾아서 그냥
play중인 뷰 stop중인 뷰 두개 만들고 transition 처리해줌


타이머 시간 표시가 이쁘게 안나온다!

숫자가 계속해서 바뀌는데 Digit의 크기가 숫자에따라 바껴서 그럼

요거 적용해주면 됩니다
monoSpaced()도 텍스트에서 같은 용도임


TabBar 백그라운드가 이상하다..!

https://sarunw.com/posts/swiftui-tabview-color/
이걸로는 해결 안됐고..


Appearnce를 수정해주는 걸로 색이 아까처럼 스크롤뷰 탭하면 달라지는 건 고쳐졌다!


결국 찾은 방법은 App파일에서 init될 때 Tabbar Appearnce 를 override해주는 거!

그러고 나서 .toolbar랑 .toolbarBackground를 적용해줌

탭바 아이템 색 바꾸는 건

AccentColor 바꿔주면 된다!!

NavigationBar Appearance도 바꿔줌!!


Cell 디자인


공부한 시간에 따라 나뉘는 이미지


TimeDetailView 디자인


리스트가 없다면 보여줄 View


백그라운드에서 타이머 실행되게 해주자

카운팅 될 때 애니메이션 뷰

profile
우주형

0개의 댓글