Skip to content

Commit 78cf59d

Browse files
authored
Merge pull request #7085 from woocommerce/feat/hack-week-ios-hot-reload
Hack Week: introduction of "hot reload" using Inject
2 parents ee78bd0 + 4606577 commit 78cf59d

File tree

6 files changed

+127
-0
lines changed

6 files changed

+127
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ Please, remember to not add this information on your commits and PRs.
110110
- Features
111111
- [In-app Feedback](docs/in-app-feedback.md)
112112
- [Card Present Payments](docs/card-present-payments.md)
113+
- Other
114+
- [Enable hot reload with Inject](docs/inject-hot-reload)
113115
114116
## 👏 Contributing
115117

WooCommerce.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

WooCommerce/Classes/AppDelegate.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import AutomatticTracks
1313

1414
import class Yosemite.ScreenshotStoresManager
1515

16+
// In that way, Inject will be available in the entire target.
17+
@_exported import Inject
18+
1619
#if DEBUG
1720
import Wormholy
1821
#endif
@@ -70,6 +73,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
7073
// Upgrade check...
7174
checkForUpgrades()
7275

76+
// Since we are using Injection for refreshing the content of the app in debug mode,
77+
// we are going to enable Inject.animation that will be used when
78+
// ever new source code is injected into our application.
79+
Inject.animation = .interactiveSpring()
80+
7381
return true
7482
}
7583

WooCommerce/Classes/ViewRelated/Hub Menu/HubMenu.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import Yosemite
66
/// and will be the entry point of the `Menu` Tab.
77
///
88
struct HubMenu: View {
9+
@ObservedObject private var iO = Inject.observer
10+
911
@ObservedObject private var viewModel: HubMenuViewModel
1012
@State private var showingWooCommerceAdmin = false
1113
@State private var showingViewStore = false
@@ -99,6 +101,7 @@ struct HubMenu: View {
99101
EmptyView()
100102
}
101103
}
104+
.enableInjection()
102105
.navigationBarHidden(true)
103106
.background(Color(.listBackground).edgesIgnoringSafeArea(.all))
104107
.onAppear {

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,7 @@
798798
4596854B254071C000D17B90 /* DownloadableFileBottomSheetListSelectorCommandTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4596854A254071C000D17B90 /* DownloadableFileBottomSheetListSelectorCommandTests.swift */; };
799799
45977EBA2603F632006CDFB8 /* MapsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45977EB92603F632006CDFB8 /* MapsHelper.swift */; };
800800
45977EC02604C167006CDFB8 /* PhoneHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45977EBF2604C167006CDFB8 /* PhoneHelper.swift */; };
801+
4598298128574688003A9AFE /* Inject in Frameworks */ = {isa = PBXBuildFile; productRef = 4598298028574688003A9AFE /* Inject */; };
801802
459D324327849B0B001155AA /* ReviewDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 459D324227849B0B001155AA /* ReviewDetailView.swift */; };
802803
459DB7D52673721300E2CAD2 /* TopLoaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 459DB7D42673721300E2CAD2 /* TopLoaderView.swift */; };
803804
459DB7E2267372ED00E2CAD2 /* TopLoaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 459DB7E1267372ED00E2CAD2 /* TopLoaderView.xib */; };
@@ -3527,6 +3528,7 @@
35273528
3FFC5EAC2851942F00563C48 /* Charts in Frameworks */,
35283529
D88FDB4525DD223B00CB0DBD /* Hardware.framework in Frameworks */,
35293530
263E37E12641AD8300260D3B /* Codegen in Frameworks */,
3531+
4598298128574688003A9AFE /* Inject in Frameworks */,
35303532
5744BEB1248FE44D000A6FE2 /* SwiftUI.framework in Frameworks */,
35313533
315E14F42698DA24000AD5FF /* PassKit.framework in Frameworks */,
35323534
174CA86A27D90A6200126524 /* AutomatticAbout in Frameworks */,
@@ -8162,6 +8164,7 @@
81628164
45455E312657C3F300BBB0C4 /* Shimmer */,
81638165
174CA86927D90A6200126524 /* AutomatticAbout */,
81648166
3FFC5EAB2851942F00563C48 /* Charts */,
8167+
4598298028574688003A9AFE /* Inject */,
81658168
);
81668169
productName = WooCommerce;
81678170
productReference = B56DB3C62049BFAA00D4AA8E /* WooCommerce.app */;
@@ -8324,6 +8327,7 @@
83248327
3F1CA81B26C3542600228BF2 /* XCRemoteSwiftPackageReference "XCUITestHelpers" */,
83258328
174CA86827D90A6200126524 /* XCRemoteSwiftPackageReference "AutomatticAbout-swift" */,
83268329
3FFC5EAA2851942F00563C48 /* XCRemoteSwiftPackageReference "Charts" */,
8330+
4598297F28574688003A9AFE /* XCRemoteSwiftPackageReference "Inject" */,
83278331
);
83288332
productRefGroup = B56DB3C72049BFAA00D4AA8E /* Products */;
83298333
projectDirPath = "";
@@ -10557,6 +10561,10 @@
1055710561
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
1055810562
MTL_ENABLE_DEBUG_INFO = YES;
1055910563
ONLY_ACTIVE_ARCH = YES;
10564+
OTHER_LDFLAGS = (
10565+
"-Xlinker",
10566+
"-interposable",
10567+
);
1056010568
SDKROOT = iphoneos;
1056110569
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
1056210570
SWIFT_OBJC_BRIDGING_HEADER = "Classes/System/WooCommerce-Bridging-Header.h";
@@ -10990,6 +10998,14 @@
1099010998
minimumVersion = 1.0.0;
1099110999
};
1099211000
};
11001+
4598297F28574688003A9AFE /* XCRemoteSwiftPackageReference "Inject" */ = {
11002+
isa = XCRemoteSwiftPackageReference;
11003+
repositoryURL = "https://github.com/krzysztofzablocki/Inject.git";
11004+
requirement = {
11005+
kind = upToNextMajorVersion;
11006+
minimumVersion = 1.1.1;
11007+
};
11008+
};
1099311009
/* End XCRemoteSwiftPackageReference section */
1099411010

1099511011
/* Begin XCSwiftPackageProductDependency section */
@@ -11051,6 +11067,11 @@
1105111067
package = 45455E302657C3F300BBB0C4 /* XCRemoteSwiftPackageReference "SwiftUI-Shimmer" */;
1105211068
productName = Shimmer;
1105311069
};
11070+
4598298028574688003A9AFE /* Inject */ = {
11071+
isa = XCSwiftPackageProductDependency;
11072+
package = 4598297F28574688003A9AFE /* XCRemoteSwiftPackageReference "Inject" */;
11073+
productName = Inject;
11074+
};
1105411075
57150E0E24F462C200E81611 /* TestKit */ = {
1105511076
isa = XCSwiftPackageProductDependency;
1105611077
productName = TestKit;

docs/inject-hot-reload.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Hot Reloading using Inject
2+
3+
In the app, we implemented an optional hot reloading functionality to speed up the development process.
4+
5+
From an interesting [article](https://merowing.info/2022/04/hot-reloading-in-swift/) of Krzysztof Zabłocki:
6+
> Hot reloading is about getting rid of compiling your whole application and avoiding deploy/restart cycles as much as possible while allowing you to edit your running application code and see changes reflected immediately.
7+
>
8+
> This process improvement can save you literally hours of development time, each day. I tracked my work for over a month, and for me, it was between 1-2h each day.
9+
10+
I found too that the hot reloading functionality is an incredible time saver, since I don't have to recompile the entire app to see a change. Indeed, I can even modify the logic within a view model to immediately see the graphic or logical implications behind a specific change.
11+
12+
## Inject
13+
For implementing the hot reloading functionality, we used [Inject](https://github.com/krzysztofzablocki/Inject), an open source library under MIT license that allow us with one or two lines of codes to enable the hot reloading functionality with `UIKit` or `SwiftUI`.
14+
15+
The library is imported using SPM.
16+
17+
## Setup
18+
For enabling the hot reload, there is a small individual developer setup that you should follow on your machine.
19+
20+
1) Download the newest version of **Xcode Injection** app from its [GitHub page](https://github.com/johnno1962/InjectionIII/releases) or from the [Mac App Store](https://apps.apple.com/app/injectioniii/id1380446739?mt=12) and install it.
21+
22+
2) Unpack it and place it under `/Applications`.
23+
24+
3) Make sure that the Xcode version you are using to compile our projects is under the default location: `/Applications/Xcode.app`. This is super important, unfortunately, Xcode Injection doesn't work if your Xcode is not under this path or it's called with a different name.
25+
26+
4) Run the injection application.
27+
28+
5) From the injection application in the menu bar, select "open project" from its menu and pick the right workspace file you are using.
29+
30+
6) Launch the WCiOS app from Xcode. If everything works properly, you should see in the console log something similar:
31+
```
32+
💉 InjectionIII connected /Users/youruserpath/woocommerce-ios/WooCommerce/WooCommerce.xcodeproj
33+
💉 Watching files under /Users/youruserpath/woocommerce-ios/WooCommerce
34+
```
35+
36+
## Enable hot reload in SwiftUI
37+
38+
Just 2 steps to enable injection in your SwiftUI Views
39+
40+
- Add `@ObservedObject private var iO = Inject.observer` to your view struct.
41+
- Call `.enableInjection()` at the end of your body definition.
42+
43+
**Remember you don't need to remove this code when you are done, it's NO-OP in production builds.**
44+
** Keep also in mind that if you try to add the injection in a view while the app is running, you will experience a crash **
45+
46+
47+
## Enable hot reload in UIKit / AppKit
48+
49+
The situation here is a little bit more complex, but still feasable.
50+
51+
If you are initializing and assigning a view or a view controller, just wrap it inside `Inject.ViewHost` or `Inject.ViewControllerHost`.
52+
53+
Case for a view:
54+
```
55+
paneA = Inject.ViewHost(
56+
PaneAView(whatever: arguments, you: want)
57+
)
58+
```
59+
60+
Case for a view controller:
61+
```
62+
let viewController = Inject.ViewControllerHost(YourViewController())
63+
rootViewController.pushViewController(viewController, animated: true)
64+
```
65+
66+
**Remember you don't need to remove this code when you are done, it's NO-OP in production builds.**
67+
** Keep also in mind that if you try to add the injection in a view while the app is running, you will experience a crash **
68+
69+
70+
## Questions
71+
72+
#### Why not use Playground?
73+
While Apple has made great strides with [Playground](https://www.apple.com/swift/playgrounds/), it is not an environment born for large projects. You can only test small pieces of code in Playground, and it's not ideal for our app. Playground was born as an environment for experimenting, and is more useful for learning.
74+
75+
#### Why not use SwiftUI preview?
76+
There are several reasons. I'd like to test everything in SwiftUI preview, but the preview functionality of SwiftUI is still broken and slow 🐌
77+
78+
If you need to test something fast, with real data, maybe in multiple flows, it's not the ideal solution. It's useful for small components (like a table row) but not too much useful for entire views or flows.
79+
Using hot reloading, allow us to have a faster workflow, and you don't need to restart the app in the simulator every time, which save you a lot of time.
80+
81+
82+
## Tips
83+
84+
- If a view embedded in a `UIHostingController` is not refreshing properly, it's because you should wrap the view controller under `Inject.ViewControllerHost`.

0 commit comments

Comments
 (0)