Skip to content

Commit

Permalink
Enable nested Optionals and Arrays of ObservableObjects (#5)
Browse files Browse the repository at this point in the history
This adds a wrapper layer which abstracts to allow containers of ObservableObjects — and adds an implementation for Optionals and Arrays.
  • Loading branch information
adam-zethraeus authored Aug 24, 2023
1 parent 7812f12 commit 05344d5
Show file tree
Hide file tree
Showing 26 changed files with 1,105 additions and 357 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2022 Adam Zethraeus
Copyright (c) 2023 Adam Zethraeus

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
23 changes: 23 additions & 0 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"pins" : [
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser",
"state" : {
"revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531",
"version" : "1.2.3"
}
},
{
"identity" : "swiftlintfix",
"kind" : "remoteSourceControl",
"location" : "https://github.com/GoodHatsLLC/SwiftLintFix.git",
"state" : {
"revision" : "df971eda06ef78e0570a8feb5e03a04c692ef2b2",
"version" : "0.1.7"
}
}
],
"version" : 2
}
66 changes: 40 additions & 26 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,32 +1,46 @@
// swift-tools-version: 5.6
// swift-tools-version: 5.8
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "Republished",
platforms: [.iOS(.v13), .macOS(.v12)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "Republished",
targets: ["Republished"]
),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "Republished",
dependencies: []
),
.testTarget(
name: "RepublishedTests",
dependencies: ["Republished"]
),
]
name: "Republished",
platforms: [.iOS(.v15), .macOS(.v13)],
products: [
.library(
name: "Republished",
targets: ["Republished"]
),
],
dependencies: [
//.package(url: "https://github.com/GoodHatsLLC/SwiftLintFix.git", from: "0.1.7"),
],
targets: [
.target(
name: "Republished",
dependencies: [],
swiftSettings: Env.swiftSettings
),
.testTarget(
name: "RepublishedTests",
dependencies: ["Republished"],
exclude: ["RepublishedTests.xctestplan"]
),
]
)

// MARK: - Env

private enum Env {
static let swiftSettings: [SwiftSetting] = {
var settings: [SwiftSetting] = []
settings.append(contentsOf: [
.enableUpcomingFeature("ConciseMagicFile"),
.enableUpcomingFeature("ExistentialAny"),
.enableUpcomingFeature("StrictConcurrency"),
.enableUpcomingFeature("ImplicitOpenExistentials"),
.enableUpcomingFeature("BareSlashRegexLiterals"),
])
return settings
}()
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
objects = {

/* Begin PBXBuildFile section */
F764635D2A97E5D9005497C3 /* OptionalExampleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F764635C2A97E5D9005497C3 /* OptionalExampleViewModel.swift */; };
F764635F2A97E73C005497C3 /* OptionalExampleContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F764635E2A97E73C005497C3 /* OptionalExampleContentView.swift */; };
F76463632A97E84C005497C3 /* ArrayExampleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76463612A97E84C005497C3 /* ArrayExampleViewModel.swift */; };
F76463642A97E84C005497C3 /* ArrayExampleContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76463622A97E84C005497C3 /* ArrayExampleContentView.swift */; };
F7B575FB2942215200FF3D38 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F7B575FA2942215200FF3D38 /* Assets.xcassets */; };
F7B575FE2942215200FF3D38 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F7B575FD2942215200FF3D38 /* Preview Assets.xcassets */; };
F7B5760C294221D200FF3D38 /* DomainModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B57606294221D200FF3D38 /* DomainModel.swift */; };
Expand All @@ -18,6 +22,10 @@
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
F764635C2A97E5D9005497C3 /* OptionalExampleViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptionalExampleViewModel.swift; sourceTree = "<group>"; };
F764635E2A97E73C005497C3 /* OptionalExampleContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OptionalExampleContentView.swift; sourceTree = "<group>"; };
F76463612A97E84C005497C3 /* ArrayExampleViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArrayExampleViewModel.swift; sourceTree = "<group>"; };
F76463622A97E84C005497C3 /* ArrayExampleContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArrayExampleContentView.swift; sourceTree = "<group>"; };
F7B575F32942215100FF3D38 /* RepublishedExampleApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RepublishedExampleApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
F7B575FA2942215200FF3D38 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
F7B575FD2942215200FF3D38 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
Expand All @@ -41,6 +49,33 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
F76463582A97E588005497C3 /* Single */ = {
isa = PBXGroup;
children = (
F7B57609294221D200FF3D38 /* ContentView.swift */,
F7B5760B294221D200FF3D38 /* ViewModel.swift */,
);
path = Single;
sourceTree = "<group>";
};
F76463592A97E5AA005497C3 /* Optional */ = {
isa = PBXGroup;
children = (
F764635E2A97E73C005497C3 /* OptionalExampleContentView.swift */,
F764635C2A97E5D9005497C3 /* OptionalExampleViewModel.swift */,
);
path = Optional;
sourceTree = "<group>";
};
F76463602A97E84C005497C3 /* Array */ = {
isa = PBXGroup;
children = (
F76463612A97E84C005497C3 /* ArrayExampleViewModel.swift */,
F76463622A97E84C005497C3 /* ArrayExampleContentView.swift */,
);
path = Array;
sourceTree = "<group>";
};
F7B575EA2942215100FF3D38 = {
isa = PBXGroup;
children = (
Expand All @@ -62,10 +97,12 @@
F7B575F52942215100FF3D38 /* RepublishedExampleApp */ = {
isa = PBXGroup;
children = (
F7B5760A294221D200FF3D38 /* App.swift */,
F76463602A97E84C005497C3 /* Array */,
F76463592A97E5AA005497C3 /* Optional */,
F76463582A97E588005497C3 /* Single */,
F7B57606294221D200FF3D38 /* DomainModel.swift */,
F7B5760B294221D200FF3D38 /* ViewModel.swift */,
F7B57607294221D200FF3D38 /* Views */,
F7B5760A294221D200FF3D38 /* App.swift */,
F7B57607294221D200FF3D38 /* UtilityViews */,
F7B575FA2942215200FF3D38 /* Assets.xcassets */,
F7B575FC2942215200FF3D38 /* Preview Content */,
);
Expand All @@ -88,13 +125,12 @@
name = Packages;
sourceTree = "<group>";
};
F7B57607294221D200FF3D38 /* Views */ = {
F7B57607294221D200FF3D38 /* UtilityViews */ = {
isa = PBXGroup;
children = (
F7B57608294221D200FF3D38 /* CapsuleButton.swift */,
F7B57609294221D200FF3D38 /* ContentView.swift */,
);
path = Views;
path = UtilityViews;
sourceTree = "<group>";
};
F7B576112942220200FF3D38 /* Frameworks */ = {
Expand Down Expand Up @@ -179,9 +215,13 @@
buildActionMask = 2147483647;
files = (
F7B57610294221D200FF3D38 /* ViewModel.swift in Sources */,
F764635D2A97E5D9005497C3 /* OptionalExampleViewModel.swift in Sources */,
F7B5760C294221D200FF3D38 /* DomainModel.swift in Sources */,
F7B5760F294221D200FF3D38 /* App.swift in Sources */,
F764635F2A97E73C005497C3 /* OptionalExampleContentView.swift in Sources */,
F76463642A97E84C005497C3 /* ArrayExampleContentView.swift in Sources */,
F7B5760D294221D200FF3D38 /* CapsuleButton.swift in Sources */,
F76463632A97E84C005497C3 /* ArrayExampleViewModel.swift in Sources */,
F7B5760E294221D200FF3D38 /* ContentView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
24 changes: 20 additions & 4 deletions RepublishedExampleApp/RepublishedExampleApp/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,25 @@ import SwiftUI
@main
struct RepublishTestApp: App {

var body: some Scene {
WindowGroup {
ContentView(viewModel: ViewModel(model: DomainModel()))
}
var body: some Scene {
WindowGroup {
// Example showing an outer ObservableObject (ViewModel) nesting an inner ObservableObject
// (DomainModel).
ContentView(viewModel: ViewModel(model: DomainModel()))

// Example showing nesting of an optional, that may or may not contain an inner
// ObservableObject
OptionalExampleContentView(viewModel: OptionalExampleViewModel(
optionalModel: Bool.random()
? DomainModel()
: nil
))

// Example showing nesting of an array, that may contain 0 to 5 inner ObservableObjects.
ArrayExampleContentView(viewModel: ArrayExampleViewModel(models: Array(
repeating: (),
count: Int.random(in: 0 ... 5)
).map { _ in DomainModel() }))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import SwiftUI

// MARK: - ArrayExampleContentView

struct ArrayExampleContentView: View {

// Regular direct use of outer ObservableObject

@StateObject var viewModel: ArrayExampleViewModel

var body: some View {
ScrollView {
VStack(alignment: .center, spacing: 24) {
Spacer()
Text(viewModel.count)
.font(.title)
.fontWeight(.bold)
.scaledToFit()
Text(viewModel.info)
.font(.body.monospaced())
Spacer()
VStack(alignment: .center, spacing: 24) {
Spacer()
CapsuleButton(
bg: (1, 0, 0, 0),
fg: 1,
text: "increment all models"
) {
viewModel.incrementAll()
}
CapsuleButton(
bg: (0, 1, 0, 0),
fg: 1,
text: "decrement all models"
) {
viewModel.decrementAll()
}
CapsuleButton(
bg: (0, 0, 0, 1),
fg: 1,
text: "zero out all models"
) {
viewModel.zeroAll()
}
Spacer()
}
}
.frame(maxWidth: .infinity)
}
.background(.gray.opacity(0.6))
}
}

// MARK: - ArrayExampleContentView_Previews

struct ArrayExampleContentView_Previews: PreviewProvider {
static var previews: some View {
ArrayExampleContentView(viewModel: ArrayExampleViewModel(models: Array(
repeating: (),
count: Int.random(in: 0 ... 5)
).map { _ in DomainModel() }))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import Republished
import SwiftUI

@MainActor
final class ArrayExampleViewModel: ObservableObject {

// MARK: Lifecycle

init(models: [DomainModel]) {
_models = .init(wrappedValue: models)
}

// MARK: Internal

var info: String {
"across \(models.count) models"
}

var count: String {
"\(models.map(\.count).reduce(0, +))"
}

func incrementAll() {
for model in models {
model.set(count: model.count + 1)
}
}

func decrementAll() {
for model in models {
model.set(count: model.count - 1)
}
}

func zeroAll() {
for model in models {
model.set(count: 0)
}
}

// MARK: Private

// Here the @Republished property wrapper is used *instead* of
// an @Published property wrapper and hold the nested ObservableObjects.
// (Note that there are no @Published wrappers in this file.)

// @Republished listens to all of its inner ObservableObjects's
// change notifications and propagates them to this containing ObservableObject.

// SwiftUI views can use properties here derived from the inner object
// just as they would use an @Published field.

@Republished private var models: [DomainModel]

}
Loading

0 comments on commit 05344d5

Please sign in to comment.