diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..033d02f Binary files /dev/null and b/.DS_Store differ diff --git a/SwiftUIBasics.xcodeproj/project.pbxproj b/SwiftUIBasics.xcodeproj/project.pbxproj index f0adfa8..0556bad 100644 --- a/SwiftUIBasics.xcodeproj/project.pbxproj +++ b/SwiftUIBasics.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 308440142B2D0A0C00291F2D /* CatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 308440132B2D0A0C00291F2D /* CatsView.swift */; }; + 308440182B2DFEB400291F2D /* ButtonNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 308440172B2DFEB400291F2D /* ButtonNavigation.swift */; }; 30CA734E2B24AE460025B2EB /* SymbolsAnimations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30CA734D2B24AE460025B2EB /* SymbolsAnimations.swift */; }; 557042892B24CB310048C81C /* ProfileListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 557042882B24CB310048C81C /* ProfileListView.swift */; }; 5570428B2B24D2F90048C81C /* TabBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5570428A2B24D2F90048C81C /* TabBarView.swift */; }; @@ -25,6 +27,8 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 308440132B2D0A0C00291F2D /* CatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CatsView.swift; sourceTree = ""; }; + 308440172B2DFEB400291F2D /* ButtonNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonNavigation.swift; sourceTree = ""; }; 30CA734D2B24AE460025B2EB /* SymbolsAnimations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SymbolsAnimations.swift; sourceTree = ""; }; 557042882B24CB310048C81C /* ProfileListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileListView.swift; sourceTree = ""; }; 5570428A2B24D2F90048C81C /* TabBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarView.swift; sourceTree = ""; }; @@ -73,6 +77,8 @@ 5570428C2B24D8050048C81C /* SignUpView.swift */, 5570428E2B24E50E0048C81C /* RatingView.swift */, 557042902B24E5220048C81C /* PlansView.swift */, + 308440132B2D0A0C00291F2D /* CatsView.swift */, + 308440172B2DFEB400291F2D /* ButtonNavigation.swift */, ); path = Views; sourceTree = ""; @@ -197,12 +203,14 @@ 5570428B2B24D2F90048C81C /* TabBarView.swift in Sources */, 5598D3162B23DF6D00A6AFD3 /* ContentView.swift in Sources */, 5598D3262B2404FC00A6AFD3 /* ProfileCardVM.swift in Sources */, + 308440182B2DFEB400291F2D /* ButtonNavigation.swift in Sources */, 5570428F2B24E50E0048C81C /* RatingView.swift in Sources */, 30CA734E2B24AE460025B2EB /* SymbolsAnimations.swift in Sources */, 5598D3142B23DF6D00A6AFD3 /* SwiftUIBasicsApp.swift in Sources */, 5598D3242B24049B00A6AFD3 /* ProfileCardView.swift in Sources */, 557042952B24F3100048C81C /* RequirementText.swift in Sources */, 557042932B24EFB00048C81C /* FormTextField.swift in Sources */, + 308440142B2D0A0C00291F2D /* CatsView.swift in Sources */, 557042892B24CB310048C81C /* ProfileListView.swift in Sources */, 557042912B24E5220048C81C /* PlansView.swift in Sources */, 5570428D2B24D8050048C81C /* SignUpView.swift in Sources */, diff --git a/SwiftUIBasics.xcodeproj/project.xcworkspace/xcuserdata/anapaulaflores.xcuserdatad/UserInterfaceState.xcuserstate b/SwiftUIBasics.xcodeproj/project.xcworkspace/xcuserdata/anapaulaflores.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..d235d3e Binary files /dev/null and b/SwiftUIBasics.xcodeproj/project.xcworkspace/xcuserdata/anapaulaflores.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/SwiftUIBasics.xcodeproj/xcuserdata/anapaulaflores.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/SwiftUIBasics.xcodeproj/xcuserdata/anapaulaflores.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..17be66e --- /dev/null +++ b/SwiftUIBasics.xcodeproj/xcuserdata/anapaulaflores.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,24 @@ + + + + + + + + + diff --git a/SwiftUIBasics.xcodeproj/xcuserdata/anapaulaflores.xcuserdatad/xcschemes/xcschememanagement.plist b/SwiftUIBasics.xcodeproj/xcuserdata/anapaulaflores.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..27cd9e4 --- /dev/null +++ b/SwiftUIBasics.xcodeproj/xcuserdata/anapaulaflores.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,14 @@ + + + + + SchemeUserState + + SwiftUIBasics.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/SwiftUIBasics/Views/ButtonNavigation.swift b/SwiftUIBasics/Views/ButtonNavigation.swift new file mode 100644 index 0000000..d912882 --- /dev/null +++ b/SwiftUIBasics/Views/ButtonNavigation.swift @@ -0,0 +1,36 @@ +// +// ButtonNavigation.swift +// SwiftUIBasics +// +// Created by Luis Ezcurdia on 16/12/23. +// + +import SwiftUI + +struct ButtonNavigation: View { + @State var isPresenting = false + var body: some View { +// NavigationView { +// NavigationLink { +// Color.indigo +// } label: { +// Button("Open", systemImage: "bolt") { +// print("hello") +// }.buttonStyle(.borderedProminent) +// } +// } + NavigationStack { + Button("Open", systemImage: "bolt") { + isPresenting = true + } + .buttonStyle(.borderedProminent) + .navigationDestination(isPresented: $isPresenting) { + Color.indigo + } + } + } +} + +#Preview { + ButtonNavigation() +} diff --git a/SwiftUIBasics/Views/CatsView.swift b/SwiftUIBasics/Views/CatsView.swift new file mode 100644 index 0000000..7222268 --- /dev/null +++ b/SwiftUIBasics/Views/CatsView.swift @@ -0,0 +1,100 @@ +// +// CatsView.swift +// SwiftUIBasics +// +// Created by Luis Ezcurdia on 15/12/23. +// + +import SwiftUI + +// Hash map cache +private class ImageCache { + static private var cache: [URL: Image] = [:] + + static subscript(url: URL) -> Image? { + get { + ImageCache.cache[url] + } + set { + ImageCache.cache[url] = newValue + } + } +} + +struct CacheAsyncImage: View where Content: View { + private let url: URL + private let scale: CGFloat + private let transaction: Transaction + private let content: (AsyncImagePhase) -> Content + + init( + url: URL, + scale: CGFloat = 1.0, + transaction: Transaction = Transaction(), + @ViewBuilder content: @escaping (AsyncImagePhase) -> Content + ) { + self.url = url + self.scale = scale + self.transaction = transaction + self.content = content + } + + var body: some View { + if let cached = ImageCache[url] { + content(.success(cached)) + } else { + AsyncImage( + url: url, + scale: scale, + transaction: transaction + ) { phase in + cacheAndRender(phase: phase) + } + } + } + + func cacheAndRender(phase: AsyncImagePhase) -> some View { + if case .success(let image) = phase { + print("Storing into cache") + ImageCache[url] = image + } + + return content(phase) + } +} + +struct CatsView: View { + var body: some View { + List { + ForEach((1..<10)) { _ in + CacheAsyncImage(url: URL(string: "https://wallpapercave.com/wp/wp3516314.jpg")!) { phase in + switch phase { + case .empty: + ProgressView() + case .success(let image): + image + .resizable() + .scaledToFit() + case .failure(let error): + EmptyView() + @unknown default: + fatalError() + } + } +// AsyncImage(url: URL(string: "https://wallpapercave.com/wp/wp3516314.jpg")) { image in +// image +// .resizable() +// .scaledToFit() +// } placeholder: { +// ProgressView() +// } +// .frame(width: .infinity, height: 120) + } + } + .listStyle(.plain) + } +} + +#Preview { + CatsView() +} diff --git a/SwiftUIBasics/Views/PlansView.swift b/SwiftUIBasics/Views/PlansView.swift index cb97876..f76ebae 100644 --- a/SwiftUIBasics/Views/PlansView.swift +++ b/SwiftUIBasics/Views/PlansView.swift @@ -9,9 +9,93 @@ import SwiftUI struct PlansView: View { var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) - } -} + + VStack { + VStack (alignment: .leading, spacing: 2) { + Text("Choose") + .font(.system(.largeTitle, design: .rounded)) + .fontWeight(.black) + Text("Your Plan") + .font(.system(.largeTitle, design: .rounded)) + .fontWeight(.black) + } + + HStack { + ZStack { + RoundedRectangle(cornerRadius: 10) + .foregroundColor(.purple) + .frame(height: 200) + VStack { + Text("Basic") + .bold() + .font(.system(.title, design: .rounded)) + Text("$9") + .bold() + .font(.system(size: 40, weight: .heavy, design: .rounded)) + Text("per month") + .font(.headline) + } + .foregroundColor(Color.white) + } + + ZStack { + RoundedRectangle(cornerRadius: 10) + .foregroundColor(Color(red: 240/255, green: 240/255, blue: 240/255)) + .frame(height: 200) + VStack { + Text("Pro") + .bold() + .font(.system(.title, design: .rounded)) + Text("$19") + .bold() + .font(.system(size: 40, weight: .heavy, design: .rounded)) + Text("per month") + .font(.headline) + } + .foregroundColor(Color.black) + + Text("Best for designer") + .font(.system(.caption, design: .rounded)) + .fontWeight(.bold) + .foregroundColor(.white) + .padding(5) + .background(Color(red: 255/255, green: 183/255, blue: 37/255)) + .offset(x: 0, y: 98) + } + } + .padding() + + ZStack { + RoundedRectangle(cornerRadius: 10) + .foregroundColor(Color(red: 62/255, green: 63/255, blue: 70/255)) + .frame(width: 360, height: 222) + VStack { + Image(systemName: "wand.and.rays") + .font(.largeTitle) + Text("Team") + .bold() + .font(.system(.title, design: .rounded)) + + Text("$299") + .bold() + .font(.system(size: 40, weight: .heavy, design: .rounded)) + Text("per month") + .font(.headline) + } + .foregroundColor(Color.white) + + Text("Perfect for teams with 20 members") + .font(.system(.caption, design: .rounded)) + .fontWeight(.bold) + .foregroundColor(.white) + .padding(5) + .background(Color(red: 255/255, green: 183/255, blue: 37/255)) + .offset(x: 0, y: 110) + } + } + } + } + #Preview { PlansView() diff --git a/SwiftUIBasics/Views/ProfileListView.swift b/SwiftUIBasics/Views/ProfileListView.swift index c3d06d9..c6a3867 100644 --- a/SwiftUIBasics/Views/ProfileListView.swift +++ b/SwiftUIBasics/Views/ProfileListView.swift @@ -16,7 +16,7 @@ struct Profile: Codable, Identifiable { struct ProfileCellView: View { let profile: Profile var body: some View { - HStack{ + HStack { Image(systemName: "person.crop.circle") .resizable() .aspectRatio(contentMode: .fill) @@ -39,7 +39,7 @@ struct ProfileCellView: View { struct ProfileListView: View { let profiles: [Profile] var body: some View { - NavigationView{ + NavigationView { List(profiles) { profile in NavigationLink { Text(profile.name) diff --git a/SwiftUIBasics/Views/RatingView.swift b/SwiftUIBasics/Views/RatingView.swift index 3ca74af..4754bfe 100644 --- a/SwiftUIBasics/Views/RatingView.swift +++ b/SwiftUIBasics/Views/RatingView.swift @@ -1,18 +1,30 @@ -// -// RatingView.swift -// SwiftUIBasics -// -// Created by Diplomado on 09/12/23. -// - import SwiftUI +struct StarRatingView: View { + @State private var rating: Int = 0 -struct RatingView: View { var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + VStack { + Text("Selected Rating: \(rating)") + + HStack { + ForEach(1...5, id: \.self) { index in + Image(systemName: index <= self.rating ? "star.fill" : "star") + .resizable() + .frame(width: 40, height: 40) + .foregroundColor(.yellow) + .onTapGesture { + self.rating = index + } + } + } + } + .padding() } } -#Preview { - RatingView() +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + StarRatingView() + } } + diff --git a/SwiftUIBasics/Views/SignUpView.swift b/SwiftUIBasics/Views/SignUpView.swift index 6cef5d9..5bb4978 100644 --- a/SwiftUIBasics/Views/SignUpView.swift +++ b/SwiftUIBasics/Views/SignUpView.swift @@ -1,56 +1,37 @@ -// -// SignUpView.swift -// SwiftUIBasics -// -// Created by Diplomado on 09/12/23. -// - import SwiftUI import Combine class SignUpViewModel: ObservableObject { // inputs - @Published var username: String = "" + @Published var email: String = "" @Published var password: String = "" @Published var passwordConfirm: String = "" // outputs - @Published var isValidUsernameLength: Bool = false - @Published var isValidPasswordLength: Bool = false - @Published var isValidPasswordUpperCase: Bool = false + @Published var isValidEmail: Bool = false + @Published var isValidPassword: Bool = false @Published var isValidPasswordMatch: Bool = false @Published var isValid: Bool = false private var cancelableSet: Set = [] init() { - $username - .receive(on: RunLoop.main) - .map { username in - return username.count >= 4 - } - .assign(to: \.isValidUsernameLength, on: self) - .store(in: &cancelableSet) - - $password + $email .receive(on: RunLoop.main) - .map { password in - return password.count >= 8 + .map { email in + let emailPattern = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" + let emailPredicate = NSPredicate(format: "SELF MATCHES %@", emailPattern) + return emailPredicate.evaluate(with: email) } - .assign(to: \.isValidPasswordLength, on: self) + .assign(to: \.isValidEmail, on: self) .store(in: &cancelableSet) $password .receive(on: RunLoop.main) .map { password in - let pattern = "[A-Z]" - if let _ = password.range(of: pattern, options: .regularExpression) { - return true - } else { - return false - } + return password.isValidPassword() } - .assign(to: \.isValidPasswordUpperCase, on: self) + .assign(to: \.isValidPassword, on: self) .store(in: &cancelableSet) Publishers.CombineLatest($password, $passwordConfirm) @@ -61,9 +42,9 @@ class SignUpViewModel: ObservableObject { .assign(to: \.isValidPasswordMatch, on: self) .store(in: &cancelableSet) - Publishers.CombineLatest4($isValidUsernameLength, $isValidPasswordLength, $isValidPasswordUpperCase, $isValidPasswordMatch) - .map { (a, b, c, d) in - return a && b && c && d + Publishers.CombineLatest3($isValidEmail, $isValidPassword, $isValidPasswordMatch) + .map { (a, b, c) in + return a && b && c } .assign(to: \.isValid, on: self) .store(in: &cancelableSet) @@ -78,21 +59,28 @@ struct SignUpView: View { Text("Create an account") .font(.system(.largeTitle, design: .rounded)) .bold() - .foregroundStyle(.maryBlue) + .foregroundColor(.maryBlue) .padding(.bottom, 30) - FormTextField(name: "Username", value: $vm.username) - RequirementText(text: "A minimum of 4 characters", isValid: vm.isValidUsernameLength) + FormTextField(name: "Email", value: $vm.email) + .keyboardType(.emailAddress) + RequirementText(text: "Valid email format", isValid: vm.isValidEmail) .padding() + FormTextField(name: "Password", value: $vm.password, isSecure: true) VStack { - RequirementText(text: "A minimum of 8 characters", isValid: vm.isValidPasswordLength) - RequirementText(text: "One uppercase letter", isValid: vm.isValidPasswordUpperCase) + RequirementText(text: "At least 8 characters", isValid: vm.isValidPassword) + RequirementText(text: "At least one uppercase letter", isValid: vm.password.containsUppercase()) + RequirementText(text: "At least one lowercase letter", isValid: vm.password.containsLowercase()) + RequirementText(text: "At least one symbol", isValid: vm.password.containsSymbol()) + RequirementText(text: "At least one number", isValid: vm.password.containsNumber()) } .padding() + FormTextField(name: "Confirm Password", value: $vm.passwordConfirm, isSecure: true) RequirementText(text: "Your confirm password should be the same as password", isValid: vm.isValidPasswordMatch) .padding() .padding(.bottom, 50) + Button(action: { print("Doing") // Proceed to the next screen @@ -104,7 +92,6 @@ struct SignUpView: View { .padding() .frame(minWidth: 0, maxWidth: .infinity) .background(vm.isValid ? .maryBlue :.turquoise) - // .background(LinearGradient(gradient: Gradient(colors: [.turquoise, .maryBlue]), startPoint: .leading, endPoint: .trailing)) .cornerRadius(10) .padding(.horizontal) } @@ -129,6 +116,33 @@ struct SignUpView: View { } } -#Preview { - SignUpView() +extension String { + func isValidPassword() -> Bool { + // Implement your password validation logic here + return self.count >= 8 + } + + func containsUppercase() -> Bool { + return self.rangeOfCharacter(from: .uppercaseLetters) != nil + } + + func containsLowercase() -> Bool { + return self.rangeOfCharacter(from: .lowercaseLetters) != nil + } + + func containsSymbol() -> Bool { + let symbolCharacterSet = CharacterSet(charactersIn: "!@#$%^&*()-_=+[]{}|;:'\",.<>/?`~") + return self.rangeOfCharacter(from: symbolCharacterSet) != nil + } + + func containsNumber() -> Bool { + return self.rangeOfCharacter(from: .decimalDigits) != nil + } +} + +struct SignUpView_Previews: PreviewProvider { + static var previews: some View { + SignUpView() + } } + diff --git a/SwiftUIBasics/Views/TabBarView.swift b/SwiftUIBasics/Views/TabBarView.swift index ed83775..f7d5172 100644 --- a/SwiftUIBasics/Views/TabBarView.swift +++ b/SwiftUIBasics/Views/TabBarView.swift @@ -34,7 +34,6 @@ struct ListView: View { } } - struct SettingsView: View { @State var vibrateOnRing: Bool = false var body: some View {