SwiftUI conditional modifier addition - swiftui

Based on a bool, I would like to add one more modifier to a Text in SwiftUI.
Ideally, I would do something like that:
Text(text)
if(true) {
.bold()
}
.foregroundColor(Color.black)
.frame(alignment: .leading)
which throws errors - the only "uncomplicated" alternative I can think of is to, depending on the bool value, create 2 different Texts. However, this results in a lot of code duplication. What can I do instead?
I've also tried declaring the Text as a let variable to access it later in the code however this prevents the element from showing up.
What IS possible is the following setup:
let title = Text("text")
.foregroundColor(Color.black)
and then in the body do
if(true) {
title
.bold()
}
However, if I add one more modifier to the declaration, it tells me Property definition has inferred type 'some View', involving the 'some' return type of another declaration

Using conditional modifiers is not recommended by Apple, as it breaks the View's identity once the condition flips. An easy alternative for your usecase would be the ternary operator:
Text(text)
.fontWeight(condition ? .bold : .regular)
.foregroundColor(Color.black)

Ditto on what Florian S said, you should use a ternary as inline conditionals on view modifiers can lead to many issues, but... they can be useful sometimes as well, so if you want to use inline conditional operations on view modifiers do this.
Add an some extensions to view.. you don't need both of these but depending on how you want to to use it, each has their strengths
extension View {
#ViewBuilder func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
if (condition) {
transform(self)
} else {
self
}
}
#ViewBuilder func `ifInline`<Content: View>(_ condition: #autoclosure () -> Bool, transform: (Self) -> Content) -> some View {
if condition() {
transform(self)
} else {
self
}
}
}
then, on the view you want to use the extension with do something like this
ForEach(values: self.multiDevice ? devices : device) { device in
Group {
ForEach(values: ColorScheme.allCases) { scheme in
self.viewToPreview
.background(Color.backgroundColor)
.colorScheme(scheme)
.previewDevice(PreviewDevice(rawValue: device))
.previewDisplayName("\(displayName) - \(scheme.previewName) - \(device)")
.if(self.wrapped) { view in
view.previewLayout(.sizeThatFits)
}
}
}
}
To use the second extension, the '.if' would turn into a '.ifInline'.
A small note, this use case is from a GenPreviews class I make in my projects to more easily show canvas previews on various devices and color schemes with a descriptive title I can provide a name for from the Provider and some bools I can pass to show either one device or multiple from two lists of options I include as well as wrapping or showing the view on a device preview.
The reason I bring this up is because this use case not only isn't used in production runtime, but isn't even included when compiling a release... which goes back to my first statement that I agree with Florian S. I have used inline conditionals on view modifiers for running code before, but it is not good practice and shouldn't be done unless circumstances require and permit it. A ternary operator for your situation would be the best approach.

Related

SwiftUI and Combine

I'm following a video on the Firebase YouTube channel. Starting around 27:45, the instructor is trying to set a variable based on a Boolean and ends up with the following code in init(task: Task):
$task
.map { task in
task.isCompleted ? "checkmark.circle.fill" : "circle"
}
.assign(to: \.completionStateIconName, on: self)
.store(in: &cancellables)
This seems overly convoluted to me. First, I can't find documentation on using .map on a struct object, only on arrays, etc. Second, what is with this &cancellables thing? (It's defined as private var cancellables = Set<AnyCancellable>() before the init{}.) Third, why all this code, and not simply:
task.completionStateIconName = task.isCompleted ? "checkmark.circle.fill" : "circle"
This seems to give the same result, but will there be something down the line that the first code fragment works, but the second doesn't?
$task (with the $ prefix) is a projected value of the #Published property wrapper, and it returns a variable of the type Published.Publisher. In other words, its a Combine publisher, which publishes a value whenever the property - in this case Task - changes.
If you didn't learn about the Combine framework (or other reactive frameworks), this answer is definitely not going to be enough. At a high-level, a Combine publisher emits values, which you can transform through operators like .map, and eventually subscribe to, for example with .sink or .assign.
So, line-by-line:
// a publisher of Task values
$task
// next, transform Task into a String using its isCompleted property
.map { task in
task.isCompleted ? "circle.fill" : "circle"
}
// subscribe, by assigning the String value to the completionStateIconName prop
.assign(to: \.completionStateIconName, on: self)
Now, the above returns an instance of AnyCancellable, which you need to retain while you want to receive the values. So you either need to store it directly as a property, or use .store to add it to a Set<AnyCancellable> - a common approach.
So, why is it so convoluted? This is, presumably, built so that if task property ever changes, the Combine pipeline would update the completionStateIconName property.
If you just did:
completionStateIconName = task.isCompleted ? "circle.fill" : "circle"
that would assign the value just in the beginning.
That being said, in this particular case it might actually be unnecessarily too convoluted to use Combine, whereas just using didSet:
var task: Task {
didSet {
completionStateIconName ? task.isCompleted ? "circle.fill" : "circle"
}
}

What is the difference between `FileDocument` and `ReferenceFileDocument` for `DocumentGroups` in SwiftUI?

I'm trying to setup a DocumentGroup in my app, but there's no examples out there yet ReferenceFileDocument is for. I know what a FileDocument is, but how are ReferenceFileDocuments different.
In the docs all it says is:
Conformance to ReferenceFileDocument is expected to be thread-safe,
and deserialization and serialization will be done on a background
thread.
There's a hint in the name: ReferenceFileDocument is a document that's a reference type (ie, a class). FileDocument is for a struct based document.
This has an effect on how documents are saved because SwiftUI can just make a copy of the reference type and save it without worrying about you coming along and modifying it during the save, since it's a value type or tree of value types.
With ReferenceFileDocument, there also doesn't seem to be a clear way for the SwiftUI to know when to save, so it depends on you telling it. There's no direct "doc is dirty, save it now" method, so the way you inform SwiftUI that you've done something that requires saving is through the undo manager.
You also need to provide a snapshot method to return a copy of the document that's safe for it to save.
final class QuizDocument: ReferenceFileDocument, ObservableObject {
#Published var quiz: QuizTemplate
init(quiz: QuizTemplate) {
self.quiz = quiz
}
static var readableContentTypes: [UTType] { [.exampleText] }
init(configuration: ReadConfiguration) throws {
guard let data = configuration.file.regularFileContents,
let quiz = try? JSONDecoder().decode(QuizTemplate.self, from: data)
else {
throw CocoaError(.fileReadCorruptFile)
}
self.quiz = quiz
}
// Produce a snapshot suitable for saving. Copy any nested references so they don't
// change while the save is in progress.
func snapshot(contentType: UTType) throws -> QuizTemplate {
return self.quiz
}
// Save the snapshot
func fileWrapper(snapshot: QuizTemplate, configuration: WriteConfiguration) throws -> FileWrapper {
let data = try JSONEncoder().encode(quiz)
return .init(regularFileWithContents: data)
}
}
ReferenceFileDocument is a document type that will auto-save in the background. It is notified of changes via the UndoManager, so in order to use it you must also make your document undo-able.
The only mention I see of it in the docs is here.
Here is a working example.

Cannot Convert From One Binding to Another

I'm developing a screen in SwiftUI and have the following code:
...
#EnvironmentObject var externalState: MainStateObject
...
SelectOptionPopover(options: $externalState.depots,
selectedOption: selectedDepot,
prompt: "Depot: ")
...
SelectOptionPopover is a view that I created to handle a variety of popovers. For the options, it expects an array of [SelectOptionPopoverOption], which is declared like this:
protocol SelectOptionPopoverOption {
var displayName: String { get }
}
Now, the issue I have is that when I pass an array of SelectOptionPopoverOptions, it works just fine. But if I pass an array of another type that conforms to SelectOptionPopoverOptions, the conversion fails with something like:
'Binding<[Depot]>' is not convertible to 'Binding<[SelectOptionPopoverOption]>'
These may be the exact same objects, but work when they're identified as SelectOptionPopoverOptions but not when identified as a Depots.
I can work around this by using arrays of SelectedOptionPopoverOption and casting them as needed, but it would sure be cleaner to be able to use the conforming types instead.
Any ideas on how I could use the more specific types instead?
You can declare and adopt your custom SelectOptionPopover view as generics
struct SelectOptionPopover<T>: View where T: SelectOptionPopoverOption {
#Binding var options: [T]
...

SwiftUI ForEach does not compile with if block inside - Bug or am I doing something wrong?

I am getting an error that I don't understand. I am not sure if it is a compiler error or if I am doing something wrong?
Inside a swiftUI View I have a list showing elements from core data (Figure 1). In the example below I replaced the t.name with "yo" for some undefined reason 😅.
Anyway, the tasks is a fetch request from Core Data:
#FetchRequest(entity: Task.entity(), sortDescriptors: []) var tasks: FetchedResults<Task>
FIGURE 1: Works fine to build and run the app.
FIGURE 2: Does not work to build and run the app.
Please help me understand what I am doing wrong or is this a compiler bug? Why can't I add the if block inside the ForEach? I can provide more information if needed. Thanks!
You can use if inside ForEach, but you should remember that ForEach is not language operator foreach but a struct type with ViewBuilder in constructor with generics in declaration, so it needs to determine type, which in your case it cannot determine.
The possible solution is to tell explicitly which type you return, as below (tested with Xcode 11.2 / iOS 13.2)
ForEach(tasks, id: \.id) { name -> Text in
if (true) {
return Text("name")
}
}
You have to return nil in case of a false condition. So you need to declare parameter as Optional and return nil in case of a false condition (XCode - 11.3.1).
ForEach(tasks, id: \.id) { t -> Text? in
return condition ? Text("text") : nil
}
}
For some reason, in the ForEach.init you're using, the view building closure isn't annotated with #ViewBuilder. This means that the if/else you're using is Swift's own if statement, not the SwiftUI construct which returns a _ConditionalContent.
I don't know if it's considered a bug by Apple, but I would definitely consider it to be one.
The easiest workaround is just to wrap the if/else in a Group - the Group.init is a #ViewBuilder, so will handle the if/else correctly.
What also worked for me was using a Group inside the ForEach like this:
ForEach(self.items, id: \.self { item in
Group {
if item.id == 0 {
Text(String(item.id))
}
}
}

Swift Delegation: unexpectedly found nil while unwrapping an Optional value

Edit: Please note the question below discusses using delegation between
2 viewcontrollers that are also implemented in a UITabBarController.
I've done a fair bit of searching here and on YouTube, but haven't seen my issue replicated elsewhere. I'll keep it to the point.
I have 2 view controllers that I coded myself -not generated by XCode-; TabOneController, and TabTwoController
below are the coding for both...
import UIKit
class TabOneController: UIViewController{
private let instanceOfTabOneView = TabOneView()
var vc1Delegate: fromOneToTwo!
override func loadView() {
super.loadView()
view.addSubview(instanceOfTabOneView.buildTheVu())
view.backgroundColor = UIColor.white
runThisOnce()
}
func runThisOnce(){
vc1Delegate.passTheValue(heroNameIs: "pass this to TabTwoController")
}
}
protocol fromOneToTwo{
func passTheValue(heroNameIs: String)
}
as for tab 2...
import UIKit
class TabTwoController: UIViewController, fromOneToTwo{
private let instanceOfTabTwoView = TabTwoView()
override func loadView() {
super.loadView()
view.addSubview(instanceOfTabTwoView.buildTheVu())
assignDelegateToSelf()
}
func assignDelegateToSelf(){
let instanceTabOne = TabOneController()
instanceTabOne.vc1Delegate = self
}
func passTheValue(heroNameIs:String){
instanceOfTabTwoView.txtFld.text = heroNameIs
}
}
I'm getting the following error at runtime -the app builds successfully-...
fatal error: unexpectedly found nil while unwrapping an Optional value
on the following line...
vc1Delegate.passTheValue(heroNameIs: "pass this to TabTwoController")
When I comment out the above line, the app builds and runs successfully, but of course the app doesn't execute the delegation.
I kinda understand what the compiler is trying to tell me, that the
vc1Delegate
hasn't been instantiated -I guess-. But I searched under every rock, and can't seem to find how to get around this.
I'd appreciate any help or guidance. Sorry if my code seems immature, I'm new to Swift and programming in general. Thank you.
In a UITabBarController, the first tab is instantiating by default. The view controller initialization executes the loadView and finds a nil because the second tab did not initialize yet; this is normal behavior. My suggestion is making the delegate weak optional with the ? suffix and run the delegate code elsewhere. Also, always capitalize the first letter in class and protocol names.
weak var vc1Delegate: FromOneToTwo?
If this structure is mandatory, try with a custom notification observer instead.
First thing first, your error happens in line var vc1Delegate: fromOneToTwo! while you declared this delegate variable as not null but then calling passTheValue on it. A correct practice will be
var vc1Delegate: fromOneToTwo?
func runThisOnce(){
if let delegate = vc1Delegate {
vc1Delegate.passTheValue(heroNameIs: "pass this to TabTwoController")
}
}
Secondly, you are not using delegate correctly. In the assignDelegateToSelf() function, you are creating a new instance of TabOneController
and then assign delegate. Instead, you need to find out the existing TabOneController instance and assign delegate.
I try this and worked add delegate = self in table cellforRowAt
like this
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "ChapterPracticeTableViewCell") as! ChapterPracticeTableViewCell
cell.deligate = self
return cell
}