diff --git a/Metatext.xcodeproj/project.pbxproj b/Metatext.xcodeproj/project.pbxproj index 7a5ea32..c12673c 100644 --- a/Metatext.xcodeproj/project.pbxproj +++ b/Metatext.xcodeproj/project.pbxproj @@ -15,6 +15,8 @@ D0091B6F24DD68090040E8D2 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0091B6D24DD68090040E8D2 /* PreferencesView.swift */; }; D0091B7124DD68220040E8D2 /* PreferencesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0091B7024DD68220040E8D2 /* PreferencesViewModel.swift */; }; D0091B7224DD68220040E8D2 /* PreferencesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0091B7024DD68220040E8D2 /* PreferencesViewModel.swift */; }; + D009CCF024F3260300F410E7 /* DecodableDefault.swift in Sources */ = {isa = PBXBuildFile; fileRef = D009CCEF24F3260300F410E7 /* DecodableDefault.swift */; }; + D009CCF124F3260300F410E7 /* DecodableDefault.swift in Sources */ = {isa = PBXBuildFile; fileRef = D009CCEF24F3260300F410E7 /* DecodableDefault.swift */; }; D0159F8624DE742F00E78478 /* TabNavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0159F8324DE742F00E78478 /* TabNavigationViewModel.swift */; }; D0159F8824DE742F00E78478 /* SecondaryNavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0159F8424DE742F00E78478 /* SecondaryNavigationViewModel.swift */; }; D0159F8A24DE742F00E78478 /* IdentitiesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0159F8524DE742F00E78478 /* IdentitiesViewModel.swift */; }; @@ -279,6 +281,7 @@ D0091B6A24DC10CE0040E8D2 /* PostingReadingPreferencesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostingReadingPreferencesViewModel.swift; sourceTree = ""; }; D0091B6D24DD68090040E8D2 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = ""; }; D0091B7024DD68220040E8D2 /* PreferencesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesViewModel.swift; sourceTree = ""; }; + D009CCEF24F3260300F410E7 /* DecodableDefault.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodableDefault.swift; sourceTree = ""; }; D0159F8324DE742F00E78478 /* TabNavigationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabNavigationViewModel.swift; sourceTree = ""; }; D0159F8424DE742F00E78478 /* SecondaryNavigationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecondaryNavigationViewModel.swift; sourceTree = ""; }; D0159F8524DE742F00E78478 /* IdentitiesViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IdentitiesViewModel.swift; sourceTree = ""; }; @@ -447,6 +450,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + D009CCEE24F325AF00F410E7 /* Property Wrappers */ = { + isa = PBXGroup; + children = ( + D009CCEF24F3260300F410E7 /* DecodableDefault.swift */, + ); + path = "Property Wrappers"; + sourceTree = ""; + }; D0159F7F24DE739000E78478 /* Views */ = { isa = PBXGroup; children = ( @@ -564,6 +575,7 @@ D047FA8524C3E21000AF17C5 /* MetatextApp.swift */, D0666A3A24C6B56200F3F04B /* Model */, D0DB6EFA24C5730600D965FE /* Networking */, + D009CCEE24F325AF00F410E7 /* Property Wrappers */, D019E6F224DF7C9E00697C7D /* Services */, D0DB6EFB24C658E400D965FE /* View Models */, D0DB6EF024C5224F00D965FE /* Views */, @@ -1125,6 +1137,7 @@ D074577A24D29366004758DB /* URLSessionConfiguration+Extensions.swift in Sources */, D0ED1BB724CE47F400B4899C /* WebAuthSession.swift in Sources */, D0A1CA7424DAC2F1003063E9 /* KingfisherOptionsInfo+Extensions.swift in Sources */, + D009CCF024F3260300F410E7 /* DecodableDefault.swift in Sources */, D054951224EB1041008B00A5 /* StatusListService.swift in Sources */, D0159F9124DE743700E78478 /* TabNavigationView.swift in Sources */, D0ED1BC424CED54D00B4899C /* HTTPTarget.swift in Sources */, @@ -1229,6 +1242,7 @@ D0ED1BC524CED54D00B4899C /* HTTPTarget.swift in Sources */, D0C963FF24CC3812003BD330 /* Publisher+Extensions.swift in Sources */, D075817D24E6659A0081F6A3 /* NotificationTypesPreferencesView.swift in Sources */, + D009CCF124F3260300F410E7 /* DecodableDefault.swift in Sources */, D019E6E824DF72E700697C7D /* AccessTokenEndpoint.swift in Sources */, D04FD73D24D4A83A007D572D /* InstanceEndpoint+Stubbing.swift in Sources */, D0DC175C24D0154F00A75C65 /* MastodonAPI.swift in Sources */, diff --git a/Shared/Databases/ContentDatabase.swift b/Shared/Databases/ContentDatabase.swift index f77cb2f..85e812e 100644 --- a/Shared/Databases/ContentDatabase.swift +++ b/Shared/Databases/ContentDatabase.swift @@ -135,10 +135,10 @@ private extension ContentDatabase { t.column("card", .blob) t.column("language", .text) t.column("text", .text) - t.column("favourited", .boolean) - t.column("reblogged", .boolean) - t.column("muted", .boolean) - t.column("bookmarked", .boolean) + t.column("favourited", .boolean).notNull() + t.column("reblogged", .boolean).notNull() + t.column("muted", .boolean).notNull() + t.column("bookmarked", .boolean).notNull() t.column("pinned", .boolean) } @@ -309,10 +309,10 @@ private struct StoredStatus: Codable, Hashable { let card: Card? let language: String? let text: String? - let favourited: Bool? - let reblogged: Bool? - let muted: Bool? - let bookmarked: Bool? + let favourited: Bool + let reblogged: Bool + let muted: Bool + let bookmarked: Bool let pinned: Bool? } diff --git a/Shared/Model/Account.swift b/Shared/Model/Account.swift index f9a18b2..2f80188 100644 --- a/Shared/Model/Account.swift +++ b/Shared/Model/Account.swift @@ -26,7 +26,7 @@ struct Account: Codable, Hashable { let headerStatic: URL let fields: [Field] let emojis: [Emoji] - let bot: Bool? - let moved: Bool? - let discoverable: Bool? + @DecodableDefault.False private(set) var bot: Bool + @DecodableDefault.False private(set) var moved: Bool + @DecodableDefault.False private(set) var discoverable: Bool } diff --git a/Shared/Model/Identity.swift b/Shared/Model/Identity.swift index 1291f04..f0d63ab 100644 --- a/Shared/Model/Identity.swift +++ b/Shared/Model/Identity.swift @@ -35,12 +35,12 @@ extension Identity { } struct Preferences: Codable, Hashable { - var useServerPostingReadingPreferences = true - var postingDefaultVisibility = Status.Visibility.public - var postingDefaultSensitive = false + @DecodableDefault.True var useServerPostingReadingPreferences + @DecodableDefault.StatusVisibilityPublic var postingDefaultVisibility: Status.Visibility + @DecodableDefault.False var postingDefaultSensitive var postingDefaultLanguage: String? - var readingExpandMedia = MastodonPreferences.ExpandMedia.default - var readingExpandSpoilers = false + @DecodableDefault.ExpandMediaDefault var readingExpandMedia: MastodonPreferences.ExpandMedia + @DecodableDefault.False var readingExpandSpoilers } } diff --git a/Shared/Model/Instance.swift b/Shared/Model/Instance.swift index b095af6..f914007 100644 --- a/Shared/Model/Instance.swift +++ b/Shared/Model/Instance.swift @@ -19,10 +19,10 @@ struct Instance: Codable, Hashable { let shortDescription: String? let email: String let version: String - let languages: [String] - let registrations: Bool? - let approvalRequired: Bool? - let invitesEnabled: Bool? + @DecodableDefault.EmptyList private(set) var languages: [String] + @DecodableDefault.False private(set) var registrations: Bool + @DecodableDefault.False private(set) var approvalRequired: Bool + @DecodableDefault.False private(set) var invitesEnabled: Bool let urls: URLs let stats: Stats let thumbnail: URL? diff --git a/Shared/Model/Poll.swift b/Shared/Model/Poll.swift index 66b938f..ad97f65 100644 --- a/Shared/Model/Poll.swift +++ b/Shared/Model/Poll.swift @@ -14,8 +14,8 @@ struct Poll: Codable, Hashable { let multiple: Bool let votesCount: Int let votersCount: Int? - let voted: Bool? - let ownVotes: [Int]? + @DecodableDefault.False private(set) var voted: Bool + @DecodableDefault.EmptyList private(set) var ownVotes: [Int] let options: [Option] let emojis: [Emoji] } diff --git a/Shared/Model/PushSubscription.swift b/Shared/Model/PushSubscription.swift index cfedd5d..88b15bd 100644 --- a/Shared/Model/PushSubscription.swift +++ b/Shared/Model/PushSubscription.swift @@ -8,7 +8,7 @@ struct PushSubscription: Codable { var favourite: Bool var reblog: Bool var mention: Bool - var poll: Bool + @DecodableDefault.True var poll: Bool } let endpoint: URL @@ -17,5 +17,10 @@ struct PushSubscription: Codable { } extension PushSubscription.Alerts { - static let initial: Self = Self(follow: true, favourite: true, reblog: true, mention: true, poll: true) + static let initial: Self = Self( + follow: true, + favourite: true, + reblog: true, + mention: true, + poll: DecodableDefault.True()) } diff --git a/Shared/Model/Status.swift b/Shared/Model/Status.swift index e08084d..76e47cd 100644 --- a/Shared/Model/Status.swift +++ b/Shared/Model/Status.swift @@ -27,7 +27,7 @@ class Status: Codable, Identifiable { let emojis: [Emoji] let reblogsCount: Int let favouritesCount: Int - let repliesCount: Int + @DecodableDefault.Zero private(set) var repliesCount: Int let application: Application? let url: URL? let inReplyToId: String? @@ -37,10 +37,10 @@ class Status: Codable, Identifiable { let card: Card? let language: String? let text: String? - let favourited: Bool? - let reblogged: Bool? - let muted: Bool? - let bookmarked: Bool? + @DecodableDefault.False private(set) var favourited: Bool + @DecodableDefault.False private(set) var reblogged: Bool + @DecodableDefault.False private(set) var muted: Bool + @DecodableDefault.False private(set) var bookmarked: Bool let pinned: Bool? // Xcode-generated memberwise initializer @@ -69,10 +69,10 @@ class Status: Codable, Identifiable { card: Card?, language: String?, text: String?, - favourited: Bool?, - reblogged: Bool?, - muted: Bool?, - bookmarked: Bool?, + favourited: Bool, + reblogged: Bool, + muted: Bool, + bookmarked: Bool, pinned: Bool?) { self.id = id self.uri = uri diff --git a/Shared/Property Wrappers/DecodableDefault.swift b/Shared/Property Wrappers/DecodableDefault.swift new file mode 100644 index 0000000..e807ee1 --- /dev/null +++ b/Shared/Property Wrappers/DecodableDefault.swift @@ -0,0 +1,97 @@ +// Copyright © 2020 Metabolist. All rights reserved. + +import Foundation + +// Thank you https://www.swiftbysundell.com/tips/default-decoding-values/ + +protocol DecodableDefaultSource { + associatedtype Value: Decodable + static var defaultValue: Value { get } +} + +enum DecodableDefault {} + +// swiftlint:disable nesting +extension DecodableDefault { + @propertyWrapper + struct Wrapper { + typealias Value = Source.Value + var wrappedValue = Source.defaultValue + } +} + +extension DecodableDefault { + typealias Source = DecodableDefaultSource + typealias List = Decodable & ExpressibleByArrayLiteral + typealias Map = Decodable & ExpressibleByDictionaryLiteral + + enum Sources { + enum True: Source { + static var defaultValue: Bool { true } + } + + enum False: Source { + static var defaultValue: Bool { false } + } + + enum EmptyString: Source { + static var defaultValue: String { "" } + } + + enum EmptyList: Source { + static var defaultValue: T { [] } + } + + enum EmptyMap: Source { + static var defaultValue: T { [:] } + } + + enum Zero: Source { + static var defaultValue: Int { 0 } + } + + enum StatusVisibilityPublic: Source { + static var defaultValue: Status.Visibility { .public } + } + + enum ExpandMediaDefault: Source { + static var defaultValue: MastodonPreferences.ExpandMedia { .default } + } + } +} +// swiftlint:enable nesting + +extension DecodableDefault { + typealias True = Wrapper + typealias False = Wrapper + typealias EmptyString = Wrapper + typealias EmptyList = Wrapper> + typealias EmptyMap = Wrapper> + typealias Zero = Wrapper + typealias StatusVisibilityPublic = Wrapper + typealias ExpandMediaDefault = Wrapper +} + +extension DecodableDefault.Wrapper: Decodable { + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + wrappedValue = try container.decode(Value.self) + } +} + +extension DecodableDefault.Wrapper: Equatable where Value: Equatable {} +extension DecodableDefault.Wrapper: Hashable where Value: Hashable {} + +extension DecodableDefault.Wrapper: Encodable where Value: Encodable { + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(wrappedValue) + } +} + +extension KeyedDecodingContainer { + func decode(_ type: DecodableDefault.Wrapper.Type, + forKey key: Key) throws -> DecodableDefault.Wrapper { + try decodeIfPresent(type, forKey: key) ?? .init() + } +}