DEV_Larva

SwiftUI Tutorials - Landmarks(7) 본문

SwiftUI & UIKit/Tutorials

SwiftUI Tutorials - Landmarks(7)

NelchuPapa 2022. 11. 8. 17:19
반응형

이제 앱의 전체적인 레이아웃과 디자인은 개발이 끝이 났다. 이번 시간에는 앱 내에서 사용자가 자신의 개성을 표현할 수 있는 프로필을 만들어 볼 것이다. 바로 시작!🧑‍💻

 

 


1. Display a User Profile

첫번째 섹션에서는 세부 정보 및 기본 설정을 로컬에서 저장하게 된다. 사용자가 세부 정보를 편집하기 전에 편집 컨트롤이 없는 요약 보기에 표시된다. 새로운 기능이 추가되는 만큼 이번 섹션에서는 특별한 문법적인 요소는 없고 새로운 파일이 많이 나올 예정이다. 

 


 
그러고나서 ProfileHost라는 View를 하나 더 추가해준다. 이 뷰의 역할은 프로필 정보의 정적 요약 뷰와 편집 모드를 모두 호스팅 하게 된다.
import Foundation

struct Profile {
    var username: String
    var prefersNotifications = true
    var seasonalPhoto = Season.winter
    var goalDate = Date()

    static let `default` = Profile(username: "g_kumar")

    enum Season: String, CaseIterable, Identifiable {
        case spring = "🌷"
        case summer = "🌞"
        case autumn = "🍂"
        case winter = "☃️"

        var id: String { rawValue }
    }
}

Profile 인스턴스를 사용하고 몇 가지 기본 사용자 정보를 표시하는 ProfileSummary라는 또 다른 View를 만든다. 이는 ProfileHost가 상태를 관리하기 때문에 이 뷰는 프로필에 대한 바인딩 대신 프로필 값을 사용하게 된다. 

import SwiftUI

struct ProfileHost: View {
    @State private var draftProfile = Profile.default

        VStack(alignment: .leading, spacing: 20) {
            ProfileSummary(profile: draftProfile)
        }
        .padding()
    }
}

struct ProfileHost_Previews: PreviewProvider {
    static var previews: some View {
        ProfileHost()
    }
}

 

import SwiftUI

struct ProfileSummary: View {
    var profile: Profile

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 10) {
                Text(profile.username)
                    .bold()
                    .font(.title)

                Text("Notifications: \(profile.prefersNotifications ? "On": "Off" )")
                Text("Seasonal Photos: \(profile.seasonalPhoto.rawValue)")
                Text("Goal Date: ") + Text(profile.goalDate, style: .date)
            }
        }
    }
}

struct ProfileSummary_Previews: PreviewProvider {
    static var previews: some View {
        ProfileSummary(profile: Profile.default)
    }
}

 

이번에는 하이킹에 대한 설명 글과 함께 배지를 구성하는 HickBadge라는 새 뷰를 만들어준다. 

import SwiftUI

struct HikeBadge: View {
    var name: String

    var body: some View {
        VStack(alignment: .center) {
            Badge()
                .frame(width: 300, height: 300)
                .scaleEffect(1.0 / 3.0)
                .frame(width: 100, height: 100)
            Text(name)
                .font(.caption)
                .accessibilityLabel("Badge for \(name).")
        }
    }
}

struct HikeBadge_Previews: PreviewProvider {
    static var previews: some View {
        HikeBadge(name: "Preview Testing")
    }
}

여러 배지를 프로필에서 추가할려면 ProfileSummary의 코드를 다시 업데이트해줘야 한다. 이전 코드에서 아래 코드를 추가해주자.

 

import SwiftUI

struct ProfileSummary: View {
    @EnvironmentObject var modelData: ModelData  //추가
    var profile: Profile

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 10) {
                Text(profile.username)
                    .bold()
                    .font(.title)

                Text("Notifications: \(profile.prefersNotifications ? "On": "Off" )")
                Text("Seasonal Photos: \(profile.seasonalPhoto.rawValue)")
                Text("Goal Date: ") + Text(profile.goalDate, style: .date)

                Divider() // 추가

                VStack(alignment: .leading) {
                    Text("Completed Badges")
                        .font(.headline)

                    ScrollView(.horizontal) {
                        HStack {
                            HikeBadge(name: "First Hike")
                            HikeBadge(name: "Earth Day")
                                .hueRotation(Angle(degrees: 90))
                            HikeBadge(name: "Tenth Hike")
                                .grayscale(0.5)
                                .hueRotation(Angle(degrees: 45))
                        }
                        .padding(.bottom)
                    }
                }

                Divider()  //추가

                VStack(alignment: .leading) {
                    Text("Recent Hikes")
                        .font(.headline)

                    HikeView(hike: modelData.hikes[0])
                }
            }
        }
    }
}

struct ProfileSummary_Previews: PreviewProvider {
    static var previews: some View {
        ProfileSummary(profile: Profile.default)
            .environmentObject(ModelData())
    }
}

이번에는 기존의 CategoryHome 파일에서 도구 모음 수정자(toolbar modifier)를 이용하여 사용자 프로필 버튼을 추가하고 사용자가 탭 하였을 때 ProgileHostView를 표시하도록 해주자. 그리고 litStyle수정자를 추가해서 콘텐츠에 더 적합한 목록 스타일을 추가해준다. 

 

import SwiftUI

struct CategoryHome: View {
    @EnvironmentObject var modelData: ModelData
    @State private var showingProfile = false  //추가

    var body: some View {
        NavigationView {
            List {
                modelData.features[0].image
                    .resizable()
                    .scaledToFill()
                    .frame(height: 200)
                    .clipped()
                    .listRowInsets(EdgeInsets())

                ForEach(modelData.categories.keys.sorted(), id: \.self) { key in
                    CategoryRow(categoryName: key, items: modelData.categories[key]!)
                }
                .listRowInsets(EdgeInsets())
            }
            .listStyle(.inset)  //추가
            .navigationTitle("Featured")
            .toolbar {  //추가
                Button {
                    showingProfile.toggle()
                } label: {
                    Label("User Profile", systemImage: "person.crop.circle")
                }
            }
            .sheet(isPresented: $showingProfile) {
                ProfileHost()
                    .environmentObject(modelData)
            }
        }
    }
}

struct CategoryHome_Previews: PreviewProvider {
    static var previews: some View {
        CategoryHome()
            .environmentObject(ModelData())
    }
}

 

 


 

2. Add an Edit Mode

사용자는 프로필 세부 정보 view나 편집뷰 사이에서 전환이 가능해야 한다. 그래서 기존의 ProfileHost에 EditButton을 추가한 다음  개별적으로 값을 편집 하기 위한 컨트롤이 있는 보기를 만들어서 편집 모드를 추가해준다. 
 

 


  1. ProfileHost파일에서 프리뷰에 환경 개체로 모델 데이터를 추가해준다. 이 뷰는 @EnvironmentObject 속성을 가진 속성을 사용하지 않지만 이 뷰의 자식인 ProfileSummary는 사용한다. 따라서 수정자가 없으면 미리보기가 실패한다.
  2. \.editMode에서 벗어나는 환경 보기 속성을 추가해주자.
  3. editMode값을 켜고 끄는 편집 버튼을 하나 만들어주면 끝난다. 
  4. @EnvironmentObject를 이용해 사용자의 프로필 데이터를 읽어 데이터 제어권을 프로필 호스트로 넘겨준다.
  5. 정적 프로필이나 editMode용  뷰를 표시하는 조건부 뷰를 추가해준다. 

 

import SwiftUI

struct ProfileHost: View {
    @Environment(\.editMode) var editMode  //추가 (2)
    @EnvironmentObject var modelData: ModelData  //추가 (4)
    @State private var draftProfile = Profile.default 

    var body: some View {
        VStack(alignment: .leading, spacing: 20) {
            HStack {  //버튼 추가 (3)
                Spacer()
                EditButton()
            }

            if editMode?.wrappedValue == .inactive { //추가 (5)
                ProfileSummary(profile: modelData.profile)  //추가 (4)
            } else {
                Text("Profile Editor")
            }
        }
        .padding()
    }
}

struct ProfileHost_Previews: PreviewProvider {
    static var previews: some View {
        ProfileHost()
            .environmentObject(ModelData())  //추가 (1)
    }
}

 

사용자가 프로필 뷰를 닫은 후에도 사용자 프로필이 지속적으로 프로필의 인스턴스를 포함하도록 ModelData클래스를 업데이트 해주자.

  @Published var profile = Profile.default

요것만 추가해주면 된다.

 


 

3. Define the Profile Editor

이번 섹션에서는 프로파일의 단순화와 일관성을 유지하기 위해 프로파일 세부 정보를 편집기에서 동일한 순서로 추가해준다. 


ProfileEditor라는 이름의 새 뷰를 하나 만들고 사용자 프로필의 초안 사본에 대한 바인딩을 넣어준다. 

 

import SwiftUI

struct ProfileEditor: View {
    @Binding var profile: Profile
    
    var body: some View {
        List {
            HStack {
                Text("Username").bold()
                Divider()
                TextField("Username", text: $profile.username)
            }
        }
    }
}

struct ProfileEditor_Previews: PreviewProvider {
    static var previews: some View {
        ProfileEditor(profile: .constant(.default))
    }
}

프로필 편집기를 포함하고 프로필 바인딩을 전달하게끔 ProfileHost의 조건부 콘텐츠를 업데이트해주자.

 

// 기존
 if editMode?.wrappedValue == .inactive {
                ProfileSummary(profile: modelData.profile) 
            } else {
                Text("Profile Editor")
           
            
//변경 
 if editMode?.wrappedValue == .inactive {
                ProfileSummary(profile: modelData.profile) 
            } else {
             ProfileEditor(profile: $draftProfile)

 

다시 ProfileEditor로 돌아와서 랜드마크 관련 이벤트에 대한 알림 수신 끄고 켤 수 있는 토글을 추가해주자. 그리고 Picker컨트롤을 이용하여 랜드마크 자신이 선호하는 계절을 선택할 수 있게 하였다. 마지막으로 계절 선택기 아래에 DatePicker를 추가해서 랜드마크 방문 목표 날짜를 직접 수정할 수 있도록 한다. 순서대로 한 줄씩 코드를 추가해보도록 하자. 

 

import SwiftUI

struct ProfileEditor: View {
    @Binding var profile: Profile
    
    var dateRange: ClosedRange<Date> {  //DatePicker 추가
        let min = Calendar.current.date(byAdding: .year, value: -1, to: profile.goalDate)!
        let max = Calendar.current.date(byAdding: .year, value: 1, to: profile.goalDate)!
        return min...max
    }
    
    var body: some View {
        List {
            HStack {
                Text("Username").bold()
                Divider()
                TextField("Username", text: $profile.username)
            }
            
            Toggle(isOn: $profile.prefersNotifications) {  //알림 토글 추가
                Text("Enable Notifications").bold()
            }
            
            VStack(alignment: .leading, spacing: 20) { // picker 컨트롤 추가
                Text("Seasonal Photo").bold()
                
                Picker("Seasonal Photo", selection: $profile.seasonalPhoto) {
                    ForEach(Profile.Season.allCases) { season in
                        Text(season.rawValue).tag(season)
                    }
                }
                .pickerStyle(.segmented)
            }

            DatePicker(selection: $profile.goalDate, in: dateRange,
            displayedComponents: .date) { //DatePicker 추가
                Text("Goal Date").bold()
            }
        }
    }
}

struct ProfileEditor_Previews: PreviewProvider {
    static var previews: some View {
        ProfileEditor(profile: .constant(.default))
    }
}

 

 


 

4. Delay Edit Propagation

사용자가 편집 모드를 종료할때까지 편집 내용이 적용되지 않도록 하기 위해 편집하는 동안 프로필의 초안 사본을 사용한 이후 사용자가 편집을 완료한 이후 초안 사본을 실제 사본에 할당해주는 과정을 한번 해볼 것이다. 

 


 

ProfileHost에 취소 버튼을 추가해줄거다. 취소 버튼은 EditButton이 제공하는 Done 버튼과 다르게 클로저의 실제 프로필 데이터에 편집 내용을 적용하지 않는다는 것이 특징이다. 그리고 onAppear(perform:) 및 onDisappear(perform:) 수정자를 적용하여 편집기를 올바른 프로필 데이터로 채우고 사용자가 완료 버튼을 탭할 때 영구 프로필을 업데이트해준다. 

 

import SwiftUI

struct ProfileHost: View {
    @Environment(\.editMode) var editMode
    @EnvironmentObject var modelData: ModelData
    @State private var draftProfile = Profile.default

    var body: some View {
        VStack(alignment: .leading, spacing: 20) {
            HStack {
                if editMode?.wrappedValue == .active {
                    Button("Cancel", role: .cancel) {
                        draftProfile = modelData.profile
                        editMode?.animation().wrappedValue = .inactive
                    }
                }
                Spacer()
                EditButton()
            }

            if editMode?.wrappedValue == .inactive {  //취소 버튼 추가
                ProfileSummary(profile: modelData.profile)
            } else {
                ProfileEditor(profile: $draftProfile)
                    .onAppear { //수정자 추가
                        draftProfile = modelData.profile
                    }
                    .onDisappear { //수정자 추가
                        modelData.profile = draftProfile
                    }
            }
        }
        .padding()
    }
}

struct ProfileHost_Previews: PreviewProvider {
    static var previews: some View {
        ProfileHost()
            .environmentObject(ModelData())
    }
}

 

 


이번 시간에는 사용자의 프로필을 수정하고 데이터를 저장하는 과정을 진행해보았다. 이다음 시간에는 프레임워크 UIkit을 추가하여 데이트 업데이트를 조정해볼 것이다. 세부적인 것은 다음 시간에 계속하도록 하고, 이번 시간에 언급되었던 수정자들도 다시 한번 복습해보는 것도 좋은 시간이 될 것 같다. 

반응형

'SwiftUI & UIKit > Tutorials' 카테고리의 다른 글

SwiftUI Tutorials - Landmarks(6)  (0) 2022.11.01
SwiftUI Tutorials - Landmarks(5)  (0) 2022.10.29
SwiftUI Tutorials - Landmarks(4)  (0) 2022.10.19
SwiftUI Tutorials - Landmarks(3)  (0) 2022.10.15
SwiftUI Tutorials - Landmarks(2)  (0) 2022.10.08