I have created my own custom classes for UIKit objects. now i want to use same classes in SwiftUI, How can i achieve that and how much effort it will take.
Also if i want i will need to write same classes in swift UI.
example, I have custom UILable subclass WMLabel.
class WMLabel: UILabel {
var myproperty: String = "" {
didSet {
self.text = myproperty
}
}
}
so how can i use WMLabel in swiftUI?
I have tried ObserverableObject and UIViewRepresentable, but not able to access the properties.
You can definitely use your UIKit classes. To get basic access to the properties, you'll want to be looking at makeUIView, which occurs when the view is first created and updateUIVew.
Using your example code:
class WMLabel: UILabel {
var myproperty: String = "" {
didSet {
self.text = myproperty
}
}
}
struct WMLabelRepresented: UIViewRepresentable {
var text : String
func makeUIView(context: Context) -> WMLabel {
return WMLabel()
}
func updateUIView(_ uiView: WMLabel, context: Context) {
uiView.myproperty = text
}
}
struct ContentView : View {
var body: some View {
WMLabelRepresented(text: "My text")
}
}
If there are things that can't be expressed declaratively, you'll want to look into coordinators and as you mentioned, possible an ObservableObject to communicate data imperatively to your view, but often you can find ways to express most things declaratively.
If you want an example of more complex imperative communication, here's a couple of links to another answers of mine:
https://stackoverflow.com/a/65926143/560942
https://stackoverflow.com/a/66845387/560942
Converting all of your custom classes and interfacing with them is going to be one heck of a chore if you have a few of them. You would end up using something called UIViewRepresentable which requires quite a few things, the most annoying of which called a coordinator. You'd almost be better off rewriting your classes into a SwiftUI version. Here's Apple's documentation on interfacing SwiftUI with UIKit: https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit
Here's an example of conversion of that UILabel into SwiftUI, with accessible properties.
Example Conversion
struct WMLabel: View {
var myProperty: String
var body: some View {
Text(myProperty)
}
}
Example Usage
struct Example: View {
var body: some View {
WMLabel(myProperty: "Hello World!")
}
}
As you can see, there is little code involved in converting something to SwiftUI, if you start getting involved in UIViewRepresentable you have to start playing with coordinators and a bunch of other interfacing methods just to make it work. Sometimes it's required, but in most cases I'd try and avoid it.
Related
I would like to create a ViewModifier where the output is conditional on the type of content it is modifying.
The best test of the concept I've managed (using Text and TextField as example View types) is as follows:
struct CustomModifier<T: View>: ViewModifier {
#ViewBuilder func body(content: Content) -> some View {
if content is Text.Type {
content.background(Color.red)
} else {
if content is TextField<T>.Type {
content.background(Color.blue)
}
}
content
}
}
The problem with the above modifier is that you need to explicitly provide the generic term when you use the modifier so seems incorrect (and, to my nose, a code smell) as you then need to define a generic on the parent View and then a generic on its parent, etc, etc..
e.g.
struct ContentView<T: View>: View {
var body: some View {
VStack {
Text("Hello world")
.modifier(CustomModifier<Text>())
TextField("Textfield", text: .constant(""))
.modifier(CustomModifier<TextField<T>>())
}
}
}
I managed to get around this problem (with some guidance from Cristik) using this extension on View:
extension View {
func customModifier() -> some View where Self:View {
modifier(CustomModifier<Self>())
}
}
The modifier was tested using the following using iPadOS Playgrounds:
struct ContentView: View {
var body: some View {
Form {
Text("Hello")
.customModifier()
TextField("Text", text: .constant(""))
.customModifier()
}
}
}
This compiles and runs but the output is not what I expected. The Text and TextField views should have different backgrounds (red and blue respectively) but they are displayed unchanged. Overriding the View type checking in the modifier (hard coding the type check to 'true') results in a background colour change so the modifier is being applied; it's the type check that's failing.
I dragged the code into Xcode to try and get a better idea of why this was happening and got an immediate compiler warning advising that the type check would always fail (the modifier name in the screenshot is different - please disregard):
Xcode compiler errors:
This explains why the code does not perform as intended but I'm unable to determine whether I have made a mistake or if there is (in real terms) no way of checking the concrete type of a View sent to a ViewModifier. As best I can tell, the content parameter sent to a ViewModifier does seem to be type erased (based on the methods accessible in Xcode) but there does seem a way of obtaining type information in this context because certain modifiers (e.g. .focused()) only operate on certain types of View (specifically, interactive text controls) and ignore others. This could of course be a private API that we can't access (yet...?)
Any guidance / explanation?
You're right, there are some code smells in that implementation, starting with the fact that you need to write type checks to accomplish the goal. Whenever you start writing is or as? along with concrete types, you should think about abstracting to a protocol.
In your case, you need an abstraction to give you the background color, so a simple protocol like:
protocol CustomModifiable: View {
var customProp: Color { get }
}
extension Text: CustomModifiable {
var customProp: Color { .red }
}
extension TextField: CustomModifiable {
var customProp: Color { .blue }
}
, should be the way to go, and the modifier should be simplifiable along the lines of:
struct CustomModifier: ViewModifier {
#ViewBuilder func body(content: Content) -> some View {
if let customModifiable = content as? CustomModifiable {
content.background(customModifiable.customProp)
} else {
content
}
}
}
The problem is that this idiomatic approach doesn't work with SwiftUI modifiers, as the content received as an argument to the body() function is some internal type of SwiftUI that wraps the original view. This means that you can't (easily) access the actual view the modifier is applied to.
And this is why the is checks always failed, as the compiler correctly said.
Not all is lost, however, as we can work around this limitation via static properties and generics.
protocol CustomModifiable: View {
static var customProp: Color { get }
}
extension Text: CustomModifiable {
static var customProp: Color { .red }
}
extension TextField: CustomModifiable {
static var customProp: Color { .blue }
}
struct CustomModifier<T: CustomModifiable>: ViewModifier {
#ViewBuilder func body(content: Content) -> some View {
content.background(T.customProp)
}
}
extension View {
func customModifier() -> some View where Self: CustomModifiable {
modifier(CustomModifier<Self>())
}
}
The above implementation comes with a compile time benefit, as only Text and TextField are allowed to be modified with the custom modifier. If the developer tries to apply the modifier on a non-accepted type of view, they-ll get a nice Instance method 'customModifier()' requires that 'MyView' conform to 'CustomModifiable', which IMO is better than deceiving about the behaviour of the modifier (i.e. does nothing of some views).
And if you need to support more views in the future, simply add extensions that conform to your protocol.
In this down code I am using Binding in a custom View called TextView, which will not use this Binding in it's body, It will just sitting there.
The things get weird when we use onChange or PreferenceKey in our project that makes changing behavior of View for unused Binding in body. So here is what happens if we don't use onChange or PreferenceKey in our project and we update that unused Binding in our project that doesn't make the view get rendered and even we don't need to defined and conform to Equitable function to help SwiftUI to understand that it is the same View. It will work even without defining Equitable function or conforming to Equitable protocol. But when we use onChange or PreferenceKey on that unused Binding and even if we use and define Equitable and Equitable function for the View it will render it any way! Even it is not necessary so I don't know how can I solve this issue!?
struct ContentView: View {
#State private var string: String = "Hello" {
didSet {
print(string)
}
}
var body: some View {
EquatableView(content: TextView(string: $string))
Button("update") { string += " updated!" }
//.onChange(of: string) { newValue in } // <<: Here!!!
}
}
struct TextView: View, Equatable {
#Binding var string: String
let value: String = "123"
var body: some View {
print("rendering TextView!", "- - - - - - ")
return Text(value)
}
static func == (lhs: TextView, rhs: TextView) -> Bool {
print("Equatable function used!")
return lhs.value == rhs.value
}
}
When you add .onChange(of: string) { ... } you are referencing string in body of ContentView, and if the value changes, body of ContentView will be executed - and thus body of TextView, too.
In your setup without .onChange you are not referencing the value of string in the body of ContentView and changes to the value do not cause ContentView to be redrawn. And with- or without EquatableView, body of TextView should not be called when clicking the button.
In your pseudo code, I think, you do not need EquatableView - because there's nothing which could be optimised, and SwiftUI's default behaviour works well.
So far, it works as expected.
Now your question boils down to: how can I avoid to redraw a child view if the super view changes? (read: how to not execute body of a child view, if body of the super view will be executed)
Well, I think, SwiftUI can do optimisations - but I don't believe it can generally omit re-drawing a child view, when the super view changes and has been redrawn.
And now, when I think about it, you may use "ContentView" as the argument to EquatableView. Since, ContenView (presumably) does not depend on string.
The Goal
The goal I have is to make a reusable view protocol that inherits the SwiftUI View protocol and provides some default functionality, for loading showing different views based on the status of the views content.
This way, I don't have to rewrite identical code for every view I create and makes code cleaner.
The Problem
I made a "DelayedContentView" protocol that displays two different view bodies based on whether the view's content has loaded. The problem arises when I try to implement it. The "loadedBody" and "unloadedBody" properties can't be of type "some View" even though they are the same type as the SwiftUI View's Body associatedType.
Background
I have a various views in my app that fetch data remotely. Each view shows two view bodies: one for when the content is being fetched and the other for when the fetch is finished.
Apple's SwiftUI View Protocol
public protocol View {
associatedtype Body : View
var body: Self.Body { get }
}
My View Protocol
protocol DelayedContentView:View {
func contentLoaded() -> Bool
var loadedBody: Self.Body { get }
var unloadedBody: Self.Body { get }
}
extension DelayedContentView {
//Default implementation that I won't have to rewrite for each view.
var body: some View {
if contentLoaded() {
return self.loadedBody
}else{
return self.unloadedBody
}
}
}
Implementation
struct ExampleView:DelayedContentView {
func contentLoaded() -> Bool {
//Ask viewModel if content is loaded.
return false
}
var loadedBody: some View {
Text("Content is loaded.")
}
var unloadedBody: some View {
Text("Fetching content...")
}
}
Compile Error: Type 'ExampleView' does not conform to protocol 'DelayedContentView'
I thought protocol inheritance was the right tool for a case like this, but maybe I'm mistaken?
The protocol DelayedContentView has to add two more associated types to represent two additional views types: one for a "loaded" view and one - for "unloaded". It cannot be Self.Body because it clearly will be violated since you need a conditional view in the body.
protocol DelayedContentView: View {
associatedtype LoadedBody: View
associatedtype UnloadedBody: View
func contentLoaded() -> Bool
var loadedBody: LoadedBody { get }
var unloadedBody: UnloadedBody { get }
}
Now that you have two of those generic types, Self.Body will be a composite type of the two:
extension DelayedContentView {
#ViewBuilder // needed to create a _ConditionalContent type from if/else
var body: some View {
if contentLoaded() {
self.loadedBody
} else {
self.unloadedBody
}
}
}
Now, your specific View will declare what LoadedView and UnloadedView types are. For example, in your ExampleView, they would both be Text, and Body would be _ConditionalContent<Text,Text>.
I learned that it is possible to render system images like so:
Image(nsImage: NSImage(imageLiteralResourceName: NSImage.addTemplateName))
But there seem to be no corresponding template name for spinner:
Do I have to use custom svg?
Xcode 12
it's just a simple view.
ProgressView()
Currently, it's defaulted to CircularProgressViewStyle but you can manually set the style of it by adding the following modifer:
.progressViewStyle(CircularProgressViewStyle())
Also, the style could be anything that conforms to ProgressViewStyle
Xcode 11
You can use NSProgressIndicator directly in SwiftUI:
Implementation
struct ProgressIndicator: NSViewRepresentable {
typealias TheNSView = NSProgressIndicator
var configuration = { (view: TheNSView) in }
func makeNSView(context: NSViewRepresentableContext<ProgressIndicator>) -> NSProgressIndicator {
TheNSView()
}
func updateNSView(_ nsView: NSProgressIndicator, context: NSViewRepresentableContext<ProgressIndicator>) {
configuration(nsView)
}
}
Sample Usage
var body: some View {
ProgressIndicator {
$0.controlTint = .blueControlTint
}
}
You can replace TheNSView with any other NSView you need to use in SwiftUI.
This icon cannot get as Image.
I have add my source code form my swift ui class:
ActivityIndicator
I'd like to implement a navigator/router for an architecture implemented with SwiftUI and Combine. In a few words the View will share viewModel with Router. When the View triggers a change on the viewModel the Router should navigate to a new sheet.
This is a version of my code where I'm directly passing the viewModel from View to Router. Is there anything wrong? My biggest doubt is that since I'm using #ObservedObject on both the Router and the View, two different instances of the viewModel are created.
VIEW MODEL
class BootViewModel:ObservableObject{
#Published var presentSignIn = false
}
VIEW
struct BootView: View {
#ObservedObject var viewModel:BootViewModel
var navigator:BootNavigator<BootView>? = nil
init(viewModel:BootViewModel) {
self.viewModel = viewModel
self.navigator = BootNavigator(view: self, viewModel: viewModel)
self.navigator.setSubscriptions()
}
var body: some View {
VStack{
Text("Hello")
Button("Button"){
self.viewModel.presentSignIn.toggle()
}
}
}
}
NAVIGATOR
class BootNavigator<T:View>{
var view:T? = nil
#ObservedObject var viewModel:BootViewModel
init(view:T, viewModel:BootViewModel) {
self.view = view
self.viewModel = viewModel
}
func setSubscriptions(){
subscribe(onSigninPressed: $viewModel.presentSignIn)
}
func subscribe(onSigninPressed : Binding<Bool>){
_ = view.sheet(isPresented: $viewModel.presentSignIn){
SignInView()
}
}
}
Why the SignInView is never presented?
Without taking into account the fact that using a router with swiftUI is not needed in general(I'm mostly doing an exercise)... is there anything wrong with this implementation?
This
view.sheet(isPresented: $viewModel.presentSignIn){
SignInView()
MUST be somewhere in body (directly or via computed property or func) but inside body's ViewBuilder
Some notes I have to point out here:
ValueType
There is a difference between an UIView and a SwiftUI View. All SwiftUI Views are value type! So they get copied when you pass them around. Be aware of that.
Single instance
If you want a single instance like a regular navigator for your entire app, you can use singleton pattern. But there is a better approach in SwiftUI universe called #Environment objects. You can take advantage of that.
Trigger a view refresh
To refresh the view (including presenting something), you must code inside the var body. But it can be directly written on indirectly through a function or etc.