I'm trying to update my MVVM-Coordinators pattern to use it with SwiftUI and Combine.
To preserve abstraction, I use a ScenesFactory that handle the creation of, well, my scenes like the following:
final class ScenesFactory {
let viewModelsFactory = SceneViewModelsFactory()
}
extension ScenesFactory: SomeFlowScenesFactory {
func makeSomeScene() -> Scene {
let someSceneInput = SomeSceneInput()
let someSceneViewModel = viewModelsFactory.makeSomeSceneViewModel(with: someSceneInput)
let someSceneView = SomeSceneView()
someSceneView.viewModel = someSceneViewModel
return BaseScene(view: someSceneView, viewModel: someSceneViewModel)
}
}
Here is the implementation of a my Scene protocol:
public protocol Scene {
var view: some View { get }
var viewModel: ViewModelOutput { get }
init(view: some View, viewModel: ViewModelOutput)
}
The goal here is to be able to use UIHostingControllerto present my someScene.view but the compiler throws an error at my Scene protocol:
I thought the point of the some keyword was precisely to use generic protocols as a return type.
What am I missing ?
I thought the point of the some keyword was precisely to use generic protocols as a return type.
Yes, but it seems that it's doesn't work that way in a protocol declaration, not really sure why.
But there is a way to fix this, use an associatedtype that is constrained to View, and the compiler stop complaining.
public protocol Scene {
associatedtype Content: View
var view: Content { get }
}
struct V: Scene {
var view: some View {
EmptyView()
}
}
Related
I use the following code snippet (in Xcode 13 Beta 5 and deployment target set to 14.0) to apply view modifiers conditionally according to iOS version:
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.modifyFor(iOS14: {
$0.onAppear {
//do some stuff
}
}, iOS15: {
$0.task { //<---- Error: 'task(priority:_:)' is only available in iOS 15.0 or newer
//do some stuff
}
})
}
}
struct CompatibleView<Input: View,
Output14: View,
Output15: View>: View {
var content: Input
var iOS14modifier: ((Input) -> Output14)?
var iOS15modifier: ((Input) -> Output15)?
#ViewBuilder var body: some View {
if #available(iOS 15, *) {
if let modifier = iOS15modifier {
modifier(content)
}
else { content }
}
else {
if let modifier = iOS14modifier {
modifier(content)
}
else { content }
}
}
}
extension View {
func modifyFor<T: View, U: View>(iOS14: ((Self) -> T)? = nil,
iOS15: ((Self) -> U)? = nil) -> some View {
CompatibleView(content: self,
iOS14modifier: iOS14,
iOS15modifier: iOS15)
}
}
this code works great as long as I don't use iOS 15's view modifiers, but if I want to use any of those modifiers (like Task for ex.) then I need to use the #available directive which's an option I don't wanna opt in, because my codebase is large, there are many parts that should adopt the new iOS 15 modifiers and by using #available everywhere in the code will make it looks like a dish of Lasagna.
how to make this piece of code compiles in a clean way and without using the #available check ?
The best solution for so far I've figured out is to add simple modify extension function for view and use that.
It's useful if availability check for modifier is needed only in one place.
If needed in more than one place, then create new modifier function.
public extension View {
func modify<Content>(#ViewBuilder _ transform: (Self) -> Content) -> Content {
transform(self)
}
}
And using it would be:
Text("Good")
.modify {
if #available(iOS 15.0, *) {
$0.badge(2)
} else {
// Fallback on earlier versions
}
}
EDIT:
#ViewBuilder
func modify<Content: View>(#ViewBuilder _ transform: (Self) -> Content?) -> some View {
if let view = transform(self), !(view is EmptyView) {
view
} else {
self
}
}
This allows us not to define fallback if not required and the view will stay untouchable.
Text("Good")
.modify {
if #available(iOS 15.0, *) {
$0.badge(2)
}
}
There is no way to do this without 'if #available', but there is a way to structure it in a somewhat clean way.
Define your own View Modifier on a wrapper View:
struct Backport<Content> {
let content: Content
}
extension View {
var backport: Backport<Self> { Backport(content: self) }
}
extension Backport where Content: View {
#ViewBuilder func badge(_ count: Int) -> some View {
if #available(iOS 15, *) {
content.badge(count)
} else {
content
}
}
}
You can then use these like this:
TabView {
Color.yellow
.tabItem {
Label("Example", systemImage: "hand.raised")
}
.backport.badge(5)
}
Blog post about it:
Using iOS-15-only View modifiers in older iOS versions
You can create a simple extension on View with #ViewBuilder
fileprivate extension View {
#ViewBuilder
var tabBarTintColor: some View {
if #available(iOS 16, *) {
self.tint(.red)
} else {
self.accentColor(.red)
}
}
}
To use it just have it chained with your existing view
TabView()
.tabBarTintColor
There is no point because even if you did back-port a modifier named task (which is how this problem is normally solved) you won’t be able to use all the magic of async/await inside which is what it was designed for. If you have a good reason for not targeting iOS 15 (I don’t know any good ones) then just continue to use onAppear as normal and either standard dispatch queue async or Combine in an #StateObject.
There is no logical use case for that modifier for the issue you are trying to solve! You have no idea, how many times your app would check your condition about availability of iOS15 in each render! Maybe 1000 of times! Insane number of control which is totally bad idea! instead use deferent Views for each scenarios like this, it would checked just one time:
WindowGroup {
if #available(iOS 15, *) {
ContentView_For_iOS15()
}
else {
ContentView_For_No_iOS15()
}
}
There are some kind of protocols in SwiftUI which they have a function, that struct should have same function to be able to conform to that protocol,
like ViewModifier protocol which need
func body(content: Content) -> some View { return content }
or Shape protocol which need
func path(in rect: CGRect) -> Path { return Path { path in } }
When we are using those struct that conform to these protocols, we actually does not provide any parameters to those required functions from protocol, they grab there need parameters magically from context.
Which this auto grab parameters is big question for me how this process happens?
There for I re created Shape protocol to actually try and test this auto grab feature! How can my function automatically grab needed parameters! Right now my code is not grabbing need data, instead of returning Shape, it returns View! which I did all the things the same like apple done! why Shape protocol of apple returns Shape but my protocol does not, also my functions does not auto grab the needed data.
import SwiftUI
struct ContentView: View {
var body: some View {
CustomShape() // print: CustomShape: (0.0, 0.0, 414.0, 405.0) without body of CustomShape: Shape!
CustomSHAPE() // it does not print! But also returns unwished View!
}
}
struct CustomShape: Shape {
//var body: some View { Color.blue } // if we have body, path would stop working!
func path(in rect: CGRect) -> Path {
print("CustomShape:", rect)
return Path { path in }
}
}
protocol SHAPE: Animatable, View { }
struct CustomSHAPE: SHAPE {
var body: some View { Color.red } // if we have body, path would stop working! also I cannot comment it like we can in CustomShape: Shape
func path(in rect: CGRect) -> Path {
print("CustomSHAPE:", rect)
return Path { path in }
}
}
I think the problem comes down thinking in terms of "auto grabbing parameters."
The Shape protocol is where path(in:) is declared for SwiftUI. View "knows" that any type conforming to Shape provides a path(in:) method that it can call with the bounds of the view into which it wants to render the shape.
Your CustomShape isn't auto-grabbing anything. It conforms to Shape so View calls its path(in:). Under the hood, hidden away from from us mere mortals who don't work at Apple, there is an NSView or UIView that View is trying to render into. After figuring out how big that underlying AppKit/UIKit view has to be, and where it is to be positioned, (ie, after applying its layout constraints), it simply passes its bounds to CustomShape's path(in:) to get a Path, which I'm sure is ultimately just a struct wrapper for a CGPath.
Your CustomSHAPE on the other hand does not conform to Shape. It conforms to a different protocol, SHAPE. View doesn't know anything about SHAPE, so it can't do anything with it. All it knows is that it conforms to View, so it has to restrict what it does with CustomSHAPE to only the things the View protocol guarantees.
Basically your custom shapes need to conform to SwiftUI's protocols for SwiftUI to know how to use them.
Now, if you write your own generic View that wraps SHAPE instances, maybe you could implement your own forwarding to SHAPE's path(in:) method, but I'm not sure off the top of my head how to implement it in a way that properly hooks into SwiftUI's rendering.
Generally speaking, you don't need magic for your protocol to be able to "auto grab" stuff. You can set default values for a protocol's declarations by using extensions. Here is a simple example:
An example without "auto grab":
protocol MyProtocol {
func doSomething() -> Bool
}
// ERRORRRR! MyStructure doesnt conform to MyProtocol because you havent defined
// the required function `func doSomething() -> Bool`
struct MyStructure: MyProtocol {
}
An example with "auto grab":
protocol MyProtocol {
func doSomething() -> Bool
}
extension MyProtocol {
func doSomething() -> Bool {
print("Im doing all i can!")
return false
}
}
// No Errors because `func doSomething() -> Bool` has defaulted to the func
// that we declared in `extension MyProtocol`. so it "auto grab" the default func.
struct MyStructure: MyProtocol {
}
Remember this was only a simple example. You can do much more complicated "auto grab"s using the power of extensions.
This is almost what is happening in Shape. Apple engineers have defined a default var body: some View for any Shape:
When you declare your own var body: some View in a type that conforms to Shape, you are overriding the default var body that Apple engineers have defined, with your insufficient var body that doesnt contain anything much.
I haven't digged into it but there is a chance that the _ShapeView<Self, ForegroundStyle> that you can see in the picture does do some actual wizardry by accessing stuff that are internal and we don't have access to yet.
EDIT:
All that being said, i digged into this more as i was curious, and here's a working example:
import SwiftUI
struct ContentView: View {
var body: some View {
CustomShape()
CustomSHAPE()
}
}
struct CustomShape: Shape {
func path(in rect: CGRect) -> Path {
print("CustomShape:", rect)
return Path { path in }
}
}
protocol SHAPE: Animatable, View {
func path(in rect: CGRect) -> Path
}
extension SHAPE {
var body: some View {
GeometryReader { geo in
self.path(in: geo.frame(in: .local))
}
}
}
struct CustomSHAPE: SHAPE {
func path(in rect: CGRect) -> Path {
print("CustomSHAPE:", rect)
return Path { path in }
}
}
in console you'll see something like this printed:
CustomShape: (0.0, 0.0, 390.0, 377.66666666666663)
CustomSHAPE: (0.0, 0.0, 390.0, 377.5)
Gif to understand easier
Is there any way to disable collapsibility of SidebarListStyle NavigationViews?
EDIT: This method still works as of late 2022, and has never stopped working on any version of macOS (up to latest Ventura 13.1). Not sure why there are answers here suggesting otherwise. If the Introspection library changes their API you may need to update your calls accordingly, but the gist of the solution is the same.
Using this SwiftUI Introspection library:
https://github.com/siteline/SwiftUI-Introspect
We can introspect the underlying NSSplitView by extending their functionality:
public func introspectSplitView(customize: #escaping (NSSplitView) -> ()) -> some View {
return introspect(selector: TargetViewSelector.ancestorOrSibling, customize: customize)
}
And then create a generic extension on View:
public extension View {
func preventSidebarCollapse() -> some View {
return introspectSplitView { splitView in
(splitView.delegate as? NSSplitViewController)?.splitViewItems.first?.canCollapse = false
}
}
}
Which can be used on our sidebar:
var body: some View {
(...)
MySidebar()
.preventSidebarCollapse()
}
The introspection library mentioned by Oskar is not working for MacOS.
Inspired by that, I figured out a solution for MacOS.
The rationality behind the solution is to use a subtle way to find out the parent view of a NavigationView which is a NSSplitViewController in the current window.
Below codes was tested on XCode 13.2 and macOS 12.1.
var body: some View {
Text("Replace with your sidebar view")
.onAppear {
guard let nsSplitView = findNSSplitVIew(view: NSApp.windows.first?.contentView), let controller = nsSplitView.delegate as? NSSplitViewController else {
return
}
controller.splitViewItems.first?.canCollapse = false
// set the width of your side bar here.
controller.splitViewItems.first?.minimumThickness = 150
controller.splitViewItems.first?.maximumThickness = 150
}
}
private func findNSSplitVIew(view: NSView?) -> NSSplitView? {
var queue = [NSView]()
if let root = view {
queue.append(root)
}
while !queue.isEmpty {
let current = queue.removeFirst()
if current is NSSplitView {
return current as? NSSplitView
}
for subview in current.subviews {
queue.append(subview)
}
}
return nil
}
While the method that Oskar used with the Introspect library no longer works, I did find another way of preventing the sidebar from collapsing using Introspect. First, you need to make an extension on View:
extension View {
public func introspectSplitView(customize: #escaping (NSSplitView) -> ()) -> some View {
return inject(AppKitIntrospectionView(
selector: { introspectionView in
guard let viewHost = Introspect.findViewHost(from: introspectionView) else {
return nil
}
return Introspect.findAncestorOrAncestorChild(ofType: NSSplitView.self, from: viewHost)
},
customize: customize
))
}
}
Then do the following:
NavigationView {
SidebarView()
.introspectSplitView { controller in
(controller.delegate as? NSSplitViewController)?.splitViewItems.first?.canCollapse = false
}
Text("Main View")
}
This being said, we don't know how long this will actually work for. Apple could change how NavigationView works and this method may stop working in the future.
I'm trying to create a 4x4 grid of images, and I'd like it to scale from 1 image up to 4.
This code works when the images provided come from a regular array
var images = ["imageOne", "imageTwo", "imageThree", "imageFour"]
However it does not work if the array comes from an object we are bound to:
#ObjectBinding var images = ImageLoader() //Where our array is in images.images
My initialiser looks like this:
init(imageUrls urls: [URL]){
self.images = ImageLoader(urls)
}
And my ImageLoader class looks like this:
class ImageLoader: BindableObject {
var didChange = PassthroughSubject<ImageLoader, Never>()
var images = [UIImage]() {
didSet{
DispatchQueue.main.async {
self.didChange.send(self)
}
}
}
init(){
}
init(_ urls: [URL]){
for image in urls{
//Download image and append to images array
}
}
}
The problem arises in my View
var body: some View {
return VStack {
if images.images.count == 1{
Image(images.images[0])
.resizable()
} else {
Text("More than one image")
}
}
}
Upon compiling, I get the error generic parameter 'FalseContent' could not be inferred, where FalseContent is part of the SwiftUI buildEither(first:) function.
Again, if images, instead of being a binding to ImageLoader, is a regular array of Strings, it works fine.
I'm not sure what is causing the issue, it seems to be caused by the binding, but I'm not sure how else to do this.
The problem is your Image initialiser, your passing a UIImage, so you should call it like this:
Image(uiImage: images.images[0])
Note that when dealing with views, flow control is a little complicated and error messages can be misleading. By commenting the "else" part of the IF statement of your view, the compiler would have shown you the real reason why it was failing.
Here is the situation. I have a protocol, and extension of it.
protocol CustomViewAddable {
var aView: UIView { get }
var bView: UIView { get }
func setupCustomView()
}
extension CustomViewAddable where Self: UIViewController {
var aView: UIView {
let _aView = UIView()
_aView.frame = self.view.bounds
_aView.backgroundColor = .grey
// this is for me to observe how many times this aView init.
print("aView: \(_aView)")
return _aView
}
var bView: UIView {
let _bView = UIView(frame: CGRect(x: 30, y: 30, width: 30, height: 30))
_bView.backgroundColor = .yellow
return _bView
}
func setupCustomView() {
view.addSubview(aView);
aView.addSubview(bView);
}
}
And I make a ViewController to conform this protocol then I add this custom 'aView' to my ViewController's view.
class MyVC: UIViewController, CustomViewAddable {
override func viewDidLoad() {
super.viewDidLoad()
setupCustomView()
}
}
I run it. In my console log it prints twice of init and I trying to do something in my custom 'aView' and it failed. (The code I paste above that I simplified so that it'll be very easy to show my intension)
Could anybody to explain why or make a fix to it that I'll be very appreciated.
Because your var aView: UIView is computed variable not store variable,
So every time you call aView, it will create a new UIView,
You can use Associated Objects in NSObject here is some tutorials:
swift-objc-runtime
associated-objects
Hope this may help.
Basically in the way you implemented the setupCustomView method nothing should work because as mentioned in another response you're using a computed property, so this implies that every time you access the property it's created again.
You don't need to use associated-objects or something like that to achieve what you want, you only need to keep the reference of the aView at the beginning avoiding calling it again, in this way:
func setupCustomView() {
let tView = aView // only is computed once
view.addSubview(tView)
tView.addSubview(bView)
}
I hope this help you.