From 649180dbb5b7d4289b9a9e96b608429fac6ddbfa Mon Sep 17 00:00:00 2001 From: Trey t Date: Sun, 13 Feb 2022 22:15:53 -0600 Subject: [PATCH] WIP - custom widget icon --- Feels.xcodeproj/project.pbxproj | 16 +- .../xcschemes/FeelsWidgetExtension.xcscheme | 2 +- FeelsWidget/FeelsWidget.swift | 21 +- Shared/Models/CustomIcon.swift | 91 ++++- Shared/Random.swift | 17 + Shared/views/CreateIconView.swift | 324 ++++++++---------- Shared/views/IconView.swift | 151 +++++--- 7 files changed, 372 insertions(+), 250 deletions(-) diff --git a/Feels.xcodeproj/project.pbxproj b/Feels.xcodeproj/project.pbxproj index f0b0ebf..21524d2 100644 --- a/Feels.xcodeproj/project.pbxproj +++ b/Feels.xcodeproj/project.pbxproj @@ -90,6 +90,8 @@ 1CD90B77278C8119001C4FEA /* LocalNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CD90B75278C8119001C4FEA /* LocalNotification.swift */; }; 1CEC966F27B9C29300CC8688 /* IconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CEC966E27B9C29300CC8688 /* IconView.swift */; }; 1CEC967127B9C2BB00CC8688 /* CustomIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CEC967027B9C2BB00CC8688 /* CustomIcon.swift */; }; + 1CEC967227B9C9FB00CC8688 /* IconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CEC966E27B9C29300CC8688 /* IconView.swift */; }; + 1CEC967327B9CA0C00CC8688 /* CustomIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CEC967027B9C2BB00CC8688 /* CustomIcon.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -297,6 +299,7 @@ 1CAD602A27A5C1C800C520BD /* Views */ = { isa = PBXGroup; children = ( + 1C358FB927B35252002C83A6 /* ActivityViewController.swift */, 1CAD602F27A5C1C800C520BD /* AddMoodHeaderView.swift */, 1CAD603127A5C1C800C520BD /* BGView.swift */, 1CAD603227A5C1C800C520BD /* ContentView.swift */, @@ -308,10 +311,9 @@ 1CAD603327A5C1C800C520BD /* HeaderStatsView.swift */, 1CEC966E27B9C29300CC8688 /* IconView.swift */, 1CAD602C27A5C1C800C520BD /* SettingsView.swift */, + 1C358FB027B0AD87002C83A6 /* SharingListView.swift */, 1CAD602B27A5C1C800C520BD /* SmallRollUpHeaderView.swift */, 1CAD603D27A6ECCD00C520BD /* SwitchableView.swift */, - 1C358FB027B0AD87002C83A6 /* SharingListView.swift */, - 1C358FB927B35252002C83A6 /* ActivityViewController.swift */, ); path = Views; sourceTree = ""; @@ -417,17 +419,17 @@ 1CD90B60278C7EBA001C4FEA /* Models */ = { isa = PBXGroup; children = ( - 1C358FB427B0ADF3002C83A6 /* SharingTemplates */, + 1C02589D27B9821700EB91AC /* CenterTiledImage.swift */, 1CA0376F2799FFA600D26164 /* ContentModeViewModel.swift */, + 1CEC967027B9C2BB00CC8688 /* CustomIcon.swift */, 1CC469AB27907D48003E0C6E /* DayChartView.swift */, 1C2618FD27960A4F00FDC148 /* FilterViewModel.swift */, 1CD90B61278C7EBA001C4FEA /* Mood.swift */, 1CD90B62278C7EBA001C4FEA /* MoodEntryExtension.swift */, + 1CB101C627B81CAC00D1C033 /* MoodMetrics.swift */, + 1C358FB427B0ADF3002C83A6 /* SharingTemplates */, 1C358FAC27ADD0C3002C83A6 /* Theme.swift */, 1C5F4977279C945E0092F1B4 /* UserDefaultsStore.swift */, - 1C02589D27B9821700EB91AC /* CenterTiledImage.swift */, - 1CEC967027B9C2BB00CC8688 /* CustomIcon.swift */, - 1CB101C627B81CAC00D1C033 /* MoodMetrics.swift */, ); path = Models; sourceTree = ""; @@ -719,6 +721,7 @@ buildActionMask = 2147483647; files = ( 1CD90B65278C7EBA001C4FEA /* Mood.swift in Sources */, + 1CEC967227B9C9FB00CC8688 /* IconView.swift in Sources */, 1CA2662D2793908700C0E12C /* Persistence.swift in Sources */, 1CD90B5F278C7EAD001C4FEA /* Random.swift in Sources */, 1CD90B68278C7EBA001C4FEA /* MoodEntryExtension.swift in Sources */, @@ -726,6 +729,7 @@ 1C10E25127A1AB320047948B /* OnboardingTitle.swift in Sources */, 1CB101C827B81CAC00D1C033 /* MoodMetrics.swift in Sources */, 1C683FCB2792281400745862 /* Stats.swift in Sources */, + 1CEC967327B9CA0C00CC8688 /* CustomIcon.swift in Sources */, 1C10E25027A1AB220047948B /* OnboardingDay.swift in Sources */, 1C10E24F27A1AB1D0047948B /* OnboardingData.swift in Sources */, 1CD90B52278C7E7A001C4FEA /* FeelsWidget.intentdefinition in Sources */, diff --git a/Feels.xcodeproj/xcshareddata/xcschemes/FeelsWidgetExtension.xcscheme b/Feels.xcodeproj/xcshareddata/xcschemes/FeelsWidgetExtension.xcscheme index 3974ccc..06acf71 100644 --- a/Feels.xcodeproj/xcshareddata/xcschemes/FeelsWidgetExtension.xcscheme +++ b/Feels.xcodeproj/xcshareddata/xcschemes/FeelsWidgetExtension.xcscheme @@ -80,7 +80,7 @@ UIImage { + let controller = UIHostingController(rootView: self) + controller.view.bounds = CGRect(origin: .zero, size: size) + let image = controller.view.asImage() + return image + } +} + +extension UIView { + func asImage() -> UIImage { + let format = UIGraphicsImageRendererFormat() + format.scale = 1 + return UIGraphicsImageRenderer(size: self.layer.frame.size, format: format).image { context in + self.drawHierarchy(in: self.layer.bounds, afterScreenUpdates: true) + } + } } extension Date { diff --git a/Shared/views/CreateIconView.swift b/Shared/views/CreateIconView.swift index 41e3569..7d93a0c 100644 --- a/Shared/views/CreateIconView.swift +++ b/Shared/views/CreateIconView.swift @@ -7,111 +7,50 @@ import SwiftUI -enum BackGroundOptions: String, CaseIterable { - case horrible - case bad - case average - case good - case great - case random - - static var selectable: [BackGroundOptions] { - return [.great, .good, .average, .bad, .horrible] - } -} - -enum Eyes { - case left - case right -} - -enum EyeOptions: String, CaseIterable { - case fire = "fire" - case bolt = "bolt2" - case dollar = "dollar" - case bell = "bell" - case btc = "btc" - case code = "code" - case crown = "crown" - case divide = "divide" - case exclamation = "exclamation" - case fan = "fan" - case floppy = "floppy" - case x = "x" - case skull = "skull" - case covid = "covid" - case bomb = "bomb" - case skull2 = "skull2" - case poo = "poo" - - static public var defaultOption: AnyView { - let image = Image(EyeOptions.fire.rawValue, bundle: .main) - .resizable() - .frame(width: 20, height: 20) - return AnyView(image) - } -} - -enum MouthOptions: String, CaseIterable { - case fire = "fire" - case bolt = "bolt2" - case dollar = "dollar" - case bell = "bell" - case btc = "btc" - case code = "code" - case crown = "crown" - case divide = "divide" - case exclamation = "exclamation" - case fan = "fan" - case floppy = "floppy" - case x = "x" - case skull = "skull" - case covid = "covid" - case bomb = "bomb" - case skull2 = "skull2" - case poo = "poo" - - static public var defaultOption: AnyView { - let image = Image(MouthOptions.bomb.rawValue, bundle: .main) - .resizable() - .frame(width: 20, height: 20) - return AnyView(image) - } -} - struct CreateIconView: View { - @State private var mouth: AnyView = MouthOptions.defaultOption - @StateObject private var customIcon = CustomIcon(leftEye: EyeOptions.defaultOption, - rightEye: EyeOptions.defaultOption, - mouth: MouthOptions.defaultOption, - background: [(AnyView, UUID)](), - bgColor: .red, - innerColor: .green) + @AppStorage(UserDefaultsStore.Keys.customIcon.rawValue, store: GroupUserDefaults.groupDefaults) private var savedCustomIcon = Data() + @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system + + static var iconViewBGs: [(Image, UUID)] = { + var blah = [(Image, UUID)]() + for _ in 0...99 { + let image = Image(BackGroundOptions.selectable.randomElement()!.rawValue, bundle: .main) + blah.append((image, UUID())) + } + return blah + }() + + @State private var mouth: Image = MouthOptions.defaultOption + @StateObject private var customIcon = CustomIcon(leftEye: EyeOptions.defaultOption, + rightEye: EyeOptions.defaultOption, + mouth: MouthOptions.defaultOption, + background: CreateIconView.iconViewBGs, + bgColor: .red, + innerColor: .green, + bgOverlayColor: .black) private var randomElements: [AnyView] = [ AnyView(Image(BackGroundOptions.selectable.randomElement()!.rawValue) - .resizable() - .frame(width: 20, height: 20)), + .resizable() + .frame(width: 20, height: 20)), AnyView(Image(BackGroundOptions.selectable.randomElement()!.rawValue) - .resizable() - .frame(width: 20, height: 20)), + .resizable() + .frame(width: 20, height: 20)), AnyView(Image(BackGroundOptions.selectable.randomElement()!.rawValue) - .resizable() - .frame(width: 20, height: 20)), + .resizable() + .frame(width: 20, height: 20)), AnyView(Image(BackGroundOptions.selectable.randomElement()!.rawValue) - .resizable() - .frame(width: 20, height: 20)) + .resizable() + .frame(width: 20, height: 20)) ] func update(eye: Eyes, eyeOption: EyeOptions) { let image = Image(eyeOption.rawValue, bundle: .main) - .resizable() - .frame(width: 20, height: 20) switch eye { case .left: - customIcon.leftEye = AnyView(image) + customIcon.leftEye = image case .right: - customIcon.rightEye = AnyView(image) + customIcon.rightEye = image } } @@ -136,9 +75,7 @@ struct CreateIconView: View { } func update(mouthOption: MouthOptions) { - let image = AnyView(Image(mouthOption.rawValue, bundle: .main) - .resizable() - .frame(width: 20, height: 20)) + let image = Image(mouthOption.rawValue, bundle: .main) customIcon.mouth = image } @@ -146,25 +83,15 @@ struct CreateIconView: View { customIcon.background.removeAll() if background == .random { - for _ in 0...120 { + for _ in 0...CustomIcon.numberOfBGItems { let image = Image(BackGroundOptions.selectable.randomElement()!.rawValue, bundle: .main) - - let sizedImage = image - .resizable() - .frame(width: 20, height: 20) - - customIcon.background.append((AnyView(sizedImage), UUID())) + customIcon.background.append((image, UUID())) } return } let image = Image(background.rawValue, bundle: .main) - - let sizedImage = image - .resizable() - .frame(width: 20, height: 20) - - for _ in 0...120 { - customIcon.background.append((AnyView(sizedImage), UUID())) + for _ in 0...CustomIcon.numberOfBGItems { + customIcon.background.append((image, UUID())) } } @@ -182,99 +109,134 @@ struct CreateIconView: View { } var iconView: some View { - IconView(customIcon: customIcon) + IconView(customIcon: customIcon, isPreview: true) } var body: some View { VStack { iconView - .padding() + .frame(width: 256, height: 256) + .cornerRadius(10) + .padding(.top) + Spacer() VStack { ColorPicker("Set the background color", selection: $customIcon.bgColor) - .padding([.leading, .trailing]) + .padding([.leading, .trailing]) ColorPicker("Set the inner color", selection: $customIcon.innerColor) - .padding([.leading, .trailing]) + .padding([.leading, .trailing]) } - HStack { - Spacer() - Menu("Left Eye") { - ForEach(EyeOptions.allCases, id: \.self) { option in - Button(action: { - update(eye: .left, eyeOption: option) - }, label: { - Label(option.rawValue, image: option.rawValue) - }) - } - } - Spacer() - Menu("Right Eye") { - ForEach(EyeOptions.allCases, id: \.self) { option in - Button(action: { - update(eye: .left, eyeOption: option) - }, label: { - Label(option.rawValue, image: option.rawValue) - }) - } - } - Spacer() - Menu("Mouth") { - ForEach(MouthOptions.allCases, id: \.self) { option in - Button(action: { - update(mouthOption: option) - }, label: { - Label(option.rawValue, image: option.rawValue) - }) - } - } - Spacer() - } - .padding(.top, 10) - - VStack{ - Text("Background") - + ZStack { + Color(theme.currentTheme.secondaryBGColor) HStack { - ForEach(BackGroundOptions.selectable, id: \.self) { bg in - Image(bg.rawValue, bundle: .main) - .resizable() - .frame(width: CGFloat(50), height: CGFloat(50), alignment: .center) - .onTapGesture { - update(background: bg) - } - } - mixBG - .onTapGesture { - update(background: .random) + Spacer() + Menu("Left Eye") { + ForEach(EyeOptions.allCases, id: \.self) { option in + Button(action: { + update(eye: .left, eyeOption: option) + }, label: { + Label(option.rawValue, image: option.rawValue) + }) } + } + Spacer() + Menu("Right Eye") { + ForEach(EyeOptions.allCases, id: \.self) { option in + Button(action: { + update(eye: .right, eyeOption: option) + }, label: { + Label(option.rawValue, image: option.rawValue) + }) + } + } + Spacer() + Menu("Mouth") { + ForEach(MouthOptions.allCases, id: \.self) { option in + Button(action: { + update(mouthOption: option) + }, label: { + Label(option.rawValue, image: option.rawValue) + }) + } + } + Spacer() } } + .frame(height: 44) .padding(.top, 10) - Button(action: { - createRandom() - }, label: { - Text("Random") - .font(.title) - .fontWeight(.bold) - .foregroundColor(Color(UIColor.white)) - .frame(minWidth: 0, maxWidth: .infinity) - .background(.blue) - }) + ZStack { + Color(theme.currentTheme.secondaryBGColor) + VStack{ + Text("Background") + + HStack { + ForEach(BackGroundOptions.selectable, id: \.self) { bg in + Image(bg.rawValue, bundle: .main) + .resizable() + .frame(minWidth: 10, idealWidth: 40, maxWidth: 40, + minHeight: 10, idealHeight: 40, maxHeight: 40, + alignment: .center) + .onTapGesture { + update(background: bg) + } + } + mixBG + .onTapGesture { + update(background: .random) + } + + ColorPicker("", selection: $customIcon.bgOverlayColor) + } + .padding([.leading, .trailing]) + } + } + .frame(height: 88) + .frame(minWidth: 0, maxWidth: .infinity) + .padding(.top, 10) - Button(action: { -// let icon = icon - }, label: { - Text("Save") - .font(.title) - .fontWeight(.bold) - .foregroundColor(Color(UIColor.white)) - .frame(minWidth: 0, maxWidth: .infinity) - .background(.green) - }) + ZStack { + VStack{ + Color(theme.currentTheme.secondaryBGColor) + Button(action: { + createRandom() + }, label: { + Text("Random") + .font(.title) + .fontWeight(.bold) + .foregroundColor(Color(UIColor.white)) + .frame(minWidth: 0, maxWidth: .infinity) + }) + .frame(height: 44) + .background(.blue) + + Button(action: { + let bigIconView = IconView(customIcon: customIcon, isPreview: false) + .frame(width: 1024, height: 1024, alignment: .center) + .aspectRatio(contentMode: .fit) + let icon = bigIconView.snapshot() + if let data = icon.pngData() { + savedCustomIcon = data + } + }, label: { + Text("Save") + .font(.title) + .fontWeight(.bold) + .foregroundColor(Color(UIColor.white)) + .frame(minWidth: 0, maxWidth: .infinity) + .background(.green) + }) + .frame(height: 44) + } + } + .frame(height: 88) } + .background( + theme.currentTheme.bg + .edgesIgnoringSafeArea(.all) + ) } } diff --git a/Shared/views/IconView.swift b/Shared/views/IconView.swift index 6ead6a2..05b94ee 100644 --- a/Shared/views/IconView.swift +++ b/Shared/views/IconView.swift @@ -10,70 +10,125 @@ import SwiftUI struct IconView: View { @State public var customIcon: CustomIcon + private let facePercSize = 0.6 + public let isPreview: Bool + + private var gridXOffset: CGFloat { + if isPreview { + return CGFloat(0) + } + return CGFloat(6) + } + + private var gridYOffset: CGFloat { + if isPreview { + return CGFloat(0) + } + return CGFloat(-8) + } + let columns = [ - GridItem(.fixed(20), spacing: 0), - GridItem(.fixed(20), spacing: 0), - GridItem(.fixed(20), spacing: 0), - GridItem(.fixed(20), spacing: 0), - GridItem(.fixed(20), spacing: 0), - GridItem(.fixed(20), spacing: 0), - GridItem(.fixed(20), spacing: 0), - GridItem(.fixed(20), spacing: 0), - GridItem(.fixed(20), spacing: 0), - GridItem(.fixed(20), spacing: 0), - GridItem(.fixed(20), spacing: 0), - GridItem(.fixed(20), spacing: 0) + GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1), + GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1), + GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1), + GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1), + GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1), + GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1), + GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1), + GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1), + GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1), + GridItem(.flexible(minimum: 1, maximum: 100), spacing: 1) ] var body: some View { - ZStack { - LazyVGrid(columns: columns, spacing: 0) { - ForEach(customIcon.background, id: \.self.1) { - $0.0 + GeometryReader { geo in + ZStack { + Rectangle() + .fill( + customIcon.bgColor + ) + .frame(width: geo.size.width, height: geo.size.height, alignment: .center) + .position(x: geo.size.width/2, y: geo.size.height/2) + + LazyVGrid(columns: columns, alignment: .leading, spacing: 0) { + ForEach(customIcon.background, id: \.self.1) { (image, uuid) in + image + .resizable() + .aspectRatio(1, contentMode: .fill) + .foregroundColor(customIcon.bgOverlayColor) + } } - } - .frame(width: 200, height: 200) - .position(x: 100, y: 110) - - - Circle() - .strokeBorder(Color.black, lineWidth: 12) - .background(Circle().fill(customIcon.innerColor)) - .frame(width: 120, height: 120, alignment: .center) - - VStack { - Spacer() - .frame(height: 70) - HStack { - Spacer() - .frame(width: 72) - customIcon.leftEye - Spacer() - .frame(width: 20) - customIcon.rightEye - Spacer() - } - Spacer() - .frame(height: 28) + .frame(width: geo.size.width, + height: geo.size.height, + alignment: .center) + .position(x: geo.size.width/2 + gridXOffset, + y: geo.size.height/2 + gridYOffset) + + .background( + .clear + ) + + Circle() + .strokeBorder(Color.black, lineWidth: geo.size.width * 0.045) + .background(Circle().fill(customIcon.innerColor)) + .frame(width: geo.size.width*facePercSize, + height: geo.size.height*facePercSize, + alignment: .center) + .position(x: geo.size.width/2, y: geo.size.height/2) + + customIcon.leftEye + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: geo.size.width*0.12, + height: geo.size.height*0.12, + alignment: .center) + .position(x: geo.size.width*0.4, + y: geo.size.height*0.4) + + customIcon.rightEye + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: geo.size.width*0.12, + height: geo.size.height*0.12, + alignment: .center) + .position(x: geo.size.width*0.6, + y: geo.size.height*0.4) + customIcon.mouth - Spacer() + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: geo.size.width*0.12, + height: geo.size.height*0.12, + alignment: .center) + .position(x: geo.size.width*0.5, + y: geo.size.height*0.59) } + .position(x: geo.size.width/2, + y: geo.size.height/2) } - .frame(width: 200, height: 200) - .background( - customIcon.bgColor - ) - .cornerRadius(10) } } struct IconView_Previews: PreviewProvider { + static var backgrounds: [(Image, UUID)] = { + var blah = [(Image, UUID)]() + for _ in 0...CustomIcon.numberOfBGItems { + let image = Image(BackGroundOptions.selectable.randomElement()!.rawValue, bundle: .main) + blah.append((image, UUID())) + } + return blah + }() + static var previews: some View { IconView(customIcon: CustomIcon(leftEye: EyeOptions.defaultOption, rightEye: EyeOptions.defaultOption, mouth: MouthOptions.defaultOption, - background: [(AnyView, UUID)](), + background: IconView_Previews.backgrounds, bgColor: .red, - innerColor: .green)) + innerColor: .green, + bgOverlayColor: .orange), + isPreview: true) + .frame(width: 256, height: 256, alignment: .center) + } }