SwiftUI: Combining Custom Text - ios

With the SwiftUI built-in Text we are able to concatenate multiple Text views if we want to create a rich Text
like this:
Text("hello").foregroundColor(.white) + Text("world").foregroundColor(.pink)
However, if I have a custom Text:
struct MyText: View {
let label: String
var body: some View {
Text(self.label)
.foregroundColor(.myAppColor)
}
}
and then combine:
MyText(label: "hello") + MyText(label: "world")
the compiler outputs the following error:
Referencing operator function '+' on 'FloatingPoint' requires that 'MyText' conform to 'FloatingPoint'
I tried casting MyText to Text but the compiler doesn't like that either.
How do I go about achieving this in a slick manner?

This is a custom feature of Text, you can, however, mimick some of the behaviour. See the following example:
import SwiftUI
struct MyText: View {
private let configurations: [Configuration]
init(_ title: String, foregroundColor: Color = .black) {
self.configurations = [
.init(title: title,
foregroundColor: foregroundColor)
]
}
private init(configurations: [Configuration]) {
self.configurations = configurations
}
private struct Configuration: Identifiable {
let id = UUID()
let title: String
let foregroundColor: Color
}
var body: some View {
HStack {
ForEach(configurations, content: Render.init)
}
}
static func + (lhs: Self, rhs: Self) -> Self {
let configurations = lhs.configurations + rhs.configurations
return MyText(configurations: configurations)
}
private struct Render: View {
let configuration: Configuration
var body: some View {
Text(configuration.title)
.foregroundColor(configuration.foregroundColor)
}
}
}
struct MyText_Previews: PreviewProvider {
static var previews: some View {
MyText("hej") + MyText("Yo", foregroundColor: .red)
}
}
This is much more a proof of concept, than an advice to do this practice however.

Related

SwiftUI extract Childview with Bindings, #State

I have the following component in a view
HStack {
TextField("New Note", text: $newNoteContent)
.onSubmit(submitNewNote)
.focused($newNoteIsFocused)
if (!newNoteContent.isEmpty) {
Button(action: submitNewNote) {
Image(systemName: "checkmark")
}
}
}
The variables are defined as follows
#State private var newNoteContent: String = ""
#FocusState private var newNoteIsFocused: Bool
func submitNewNote() {
Note.add(content: newNoteContent)
newNoteContent = ""
newNoteIsFocused = false
}
I would like to extract it and make it either a computed variable returning a view or a function that returns a view (I dont know which is better). I want to extract it because I reuse a similar struct.
Full code in case its needed: https://github.com/charelF/RemindMeApp/blob/main/RemindMe/NotesView.swift
I have tried the following:
func editCell(
noteContent: Binding<String>,
submitFunc: #escaping () -> (),
focus: FocusState<Bool>.Binding
) -> some View {
return HStack {
TextField("New Note", text: noteContent)
.onSubmit(submitFunc)
.focused(focus)
if (!noteContent.isEmpty) {
Button(action: submitFunc) {
Image(systemName: "checkmark")
}
}
}
}
But there are some errors and its generally just playing around -- I have no idea what I am really doing here and need some feedback/help.
Update from comment:
So I extracted the view as follows for now
struct ExtractedView: View {
#State private var editNoteContent: String = ""
#FocusState private var editNoteIsFocused: Bool
#State private var editNote: Note? = nil
func editExistingNote(note: Note?) {
guard let note else { return }
note.content = editNoteContent
PersistenceController.shared.save()
editNoteContent = ""
editNoteIsFocused = false
editNote = nil
}
But I dont understand how to call it. If I call it with ExtractedView() then the code compiles and the app runs, but it crashes when I enter this path of the app. And when I call it like this:
ExtractedView(
editNoteContent: editNoteContent,
editNoteIsFocused: editNoteIsFocused,
editNote: editNote
)
Then i get lots of errors ...
Thanks to the comments and this answer here: https://developer.apple.com/forums/thread/682448 I got it to work. I extracted the view as follows:
struct EditNoteView: View {
#Binding var noteContent: String
#FocusState var isNoteFocused: Bool
var onsubmit: () -> ()
var placeholder: String
var body: some View {
HStack {
TextField(placeholder, text: $noteContent)
.onSubmit(onsubmit)
.focused($isNoteFocused)
if (!noteContent.isEmpty) {
Button(action: onsubmit) {
Image(systemName: "checkmark")
}
}
}
}
}
and then I call this child view from my parent view (inside the body) with
EditNoteView(
noteContent: $newNoteContent,
isNoteFocused: _newNoteIsFocused,
onsubmit: submitNewNote,
placeholder: "New Note"
)
Also in my parent views, I have the following definitions for the variables
#State private var newNoteContent: String = ""
#FocusState private var newNoteIsFocused: Bool
func submitNewNote() {
Note.add(content: newNoteContent)
newNoteContent = ""
newNoteIsFocused = false
}
The main takeaways is that all of the #State things map to #Binding in the childview, and the #FocusState maps to another #FocusState, but there is a _ required before the parameter in the call.

Work with Binding inside an external function in SwiftUI

I'm currently learning SwiftUI and want to develop my own app. I have designed a LoginView and a LoginHandler that should take care of all the logic behind a login. When the user enters the wrong username/password, an Alert should appear on the screen. I solved this with the state variable loginError. But now comes the tricky part, as i want to pass a binding of this variable to my login function in the LoginHandler. Take a look at the following code:
import SwiftUI
struct LoginView: View
{
#EnvironmentObject var loginHandler: LoginHandler
#State private var username: String = ""
#State private var password: String = ""
#State private var loginError: Bool = false
...
private func login()
{
loginHandler.login(username: username, password: password, error: $loginError)
}
}
I am now trying to change the value of error inside my login function:
import Foundation
import SwiftUI
class LoginHandler: ObservableObject
{
public func login(username: String, password: String, error: Binding<Bool>)
{
error = true
}
}
But I'm getting the error
Cannot assign to value: 'error' is a 'let' constant
which makes sense I think because you can't edit the parameters in swift. I have also tried _error = true because I once saw the underscore in combination with a binding, but this doesn't worked either.
But then I came up with a working solution: error.wrappedValue = true. My only problem with that is the following statement from Apples Developer Documentation:
This property provides primary access to the value’s data. However, you don’t access wrappedValue directly. Instead, you use the property variable created with the #Binding attribute.
Although I'm super happy that it works, I wonder if there is any better way to solve this situation?
Update 20.3.21: New edge case
In the comment section I mentioned a case where you don't know how many times your function will be used. I will now provide a little code example:
Imagine a list of downloadable files (DownloadView) that you will get from your backend:
import SwiftUI
struct DownloadView: View
{
#EnvironmentObject var downloadHandler: DownloadHandler
var body: some View
{
VStack
{
ForEach(downloadHandler.getAllDownloadableFiles())
{
file in DownloadItemView(file: file)
}
}
}
}
Every downloadable file has a name, a small description and its own download button:
import SwiftUI
struct DownloadItemView: View
{
#EnvironmentObject var downloadHandler: DownloadHandler
#State private var downloadProgress: Double = -1
var file: File
var body: some View
{
HStack
{
VStack
{
Text(file.name)
Text(file.description)
}
Spacer()
if downloadProgress < 0
{
// User can start Download
Button(action: {
downloadFile()
})
{
Text("Download")
}
}
else
{
// User sees download progress
ProgressView(value: $downloadProgress)
}
}
}
func downloadFile()
{
downloadHandler.downloadFile(file: file, progress: $downloadProgress)
}
}
And now finally the 'DownloadHandler':
import Foundation
import SwiftUI
class DownloadHandler: ObservableObject
{
public func downloadFile(file: File, progress: Binding<Double>)
{
// Example for changing the value
progress = 0.5
}
}
You can update parameters of a function as well, here is an example, this not using Binding or State, it is inout!
I am now trying to change the value of error inside my login function:
Cannot assign to value: 'error' is a 'let' constant
So with this method or example you can!
struct ContentView: View {
#State private var value: String = "Hello World!"
var body: some View {
Text(value)
.padding()
Button("update") {
testFuction(value: &value)
}
}
}
func testFuction(value: inout String) {
value += " updated!"
}
I see what you're trying to do, but it will cause problems later on down the line because you're dealing with State here. Now one solution would be:
You could just abstract the error to the class, but then you would have the username and password in one spot and the error in another.
The ideal solution then is to abstract it all away in the same spot. Take away all of the properties from your view and have it like this:
import SwiftUI
struct LoginView: View
{
#EnvironmentObject var loginHandler: LoginHandler
// login() <-- Call this when needed
...
}
Then in your class:
import Foundation
import SwiftUI
#Published error: Bool = false
var username = ""
var password = ""
class LoginHandler: ObservableObject
{
public func login() {
//If you can't login then throw your error here
self.error = true
}
}
The only left for you to do is to update the username and password` and you can do that with this for example
TextField("username", text: $loginHandler.username)
TextField("username", text: $loginHandler.password)
Edit: Adding an update for the edge case:
import SwiftUI
struct ContentView: View {
var body: some View {
LazyVGrid(columns: gridModel) {
ForEach(0..<20) { x in
CustomView(id: x)
}
}
}
let gridModel = [GridItem(.adaptive(minimum: 100, maximum: 100), spacing: 10),
GridItem(.adaptive(minimum: 100, maximum: 100), spacing: 10),
GridItem(.adaptive(minimum: 100, maximum: 100), spacing: 10)
]
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct CustomView: View {
#State private var downloaded = false
#State private var progress = 0
#ObservedObject private var viewModel = StateManager()
let id: Int
var body: some View {
showAppropriateView()
}
#ViewBuilder private func showAppropriateView() -> some View {
if viewModel.downloadStates[id] == true {
VStack {
Circle()
.fill(Color.blue)
.frame(width: 50, height: 50)
}
} else {
Button("Download") {
downloaded = true
viewModel.saveState(of: id, downloaded)
}
}
}
}
final class StateManager: ObservableObject {
#Published var downloadStates: [Int : Bool] = [:] {
didSet { print(downloadStates)}
}
func saveState(of id: Int,_ downloaded: Bool) {
downloadStates[id] = downloaded
}
}
I didn't add the progress to it because I'm short on time but I think this conveys the idea. You can always abstract away the individual identity needed by other views.
Either way, let me know if this was helpful.

(SwiftUI) How can I access the match I have found from one array in another in ViewBuilder?

I have two arrays.
maschineItems: [MaschineItem]
maschines: [Maschine]
Both objects have the property "name"
In my View I want to check whether the name of a maschineItem exists in the array of the machines. I want to make a 'light Bubble' for all the maschineItems which exists in machines and a 'dark Bubble' for all the maschineItems which doesnt exist in machines.
Here is my code (which works):
import SwiftUI
import Combine
struct OverviewView: View {
#FetchRequest(fetchRequest: MaschineItem.getAllMaschines()) var maschineItems:FetchedResults<MaschineItem>
#State var networkManager = NetworkManager()
var body: some View {
NavigationView{
ScrollView{
VStack(spacing: 30) {
ForEach(maschineItems) { maschineItem in
if NetworkManager.maschines.contains(where: {$0.name == maschineItem.name}) {
BubbleView(locationText: "TEST LOCATION", textIo: "/", textnIo: "/", maschineItem: maschineItem, color: .dividerBackground, opacity: 0.25)
}else {
BubbleView(locationText: "Unknown device", textIo: "/", textnIo: "/", maschineItem: maschineItem, color: .gray, opacity: 0.5)
}
}
}
}
.navigationBarTitle("Übersicht", displayMode: .large)
}
}
Now I want to let the Bubbles contains information about the machines. So I have to initialize the match before the if statement, isn't it?
But when I do so:
var body: some View {
NavigationView{
ScrollView{
VStack(spacing: 30) {
ForEach(maschineItems) { maschineItem in
if let maschines = NetworkManager.maschines.first(where: {$0.name == maschineItem.name}) {
BubbleView(locationText: "\(maschines.location.building) / \(maschines.location.workcell)", textIo: "\(maschines.resultCountiO)", textnIo: "\(maschines.location.resultCountniO)", maschineItem: maschineItem, color: .dividerBackground, opacity: 0.5)
}else {
BubbleView(locationText: "Unknown device", textIo: "/", textnIo: "/", maschineItem: maschineItem, color: .gray, opacity: 0.5)
}
}
}
}
.navigationBarTitle("Übersicht", displayMode: .large)
}
}
I get the following error in the line of the if statement:
Closure containing control flow statement cannot be used with function builder 'ViewBuilder'
How can I fix this problem? I researched a lot and I found similar threads, which showed me that I have to use the array.first(where: {}) method, but I found nothing which helped me with this problem. And yes, I tried to make a function because logic is wrong in views. But when I write all this in a func, than the same error occurs.
I am really thankful for all who tries to help me.
PS: Im German, sorry for the English :D
I would use a computed variable that provides the data you need, avoiding the if let. In my example I have added a computed variable (matchedMachines) which is an array of tuples. The first value in the tuple is the Machine instance. The second value is a Bool that indicates whether the machine was found or not.
//
// ContentView.swift
// BubbleTest
//
// Created by Paul Wilkinson on 26/6/20.
//
import SwiftUI
struct Machine: Identifiable {
let id = UUID()
let name: String
let colour: String
}
struct ContentView: View {
var machines = [Machine(name: "Bob", colour: "Grey"), Machine(name:"Susan",colour: "Purple"), Machine(name: "Mary", colour: "Red"), Machine(name: "Peter", colour: "Green")]
var names = ["Bob","Mary"]
var matchedMachines: [(Machine,Bool)] {
get {
return machines.map { ($0, names.contains($0.name))
}
}
}
var body: some View {
VStack(spacing:30) {
ForEach(matchedMachines, id: \.0.id ) { machineTuple in
BubbleView(bubbleColor: machineTuple.1 ? .blue:.gray) {
Text(machineTuple.0.name)
Text("Location: \(machineTuple.1 ? "Test Location" : "Location Unknown")")
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct BubbleView<Content: View>: View {
let content: Content
var bubbleColor: Color
init(bubbleColor: Color, #ViewBuilder content: () -> Content) {
self.bubbleColor = bubbleColor
self.content = content()
}
var body: some View {
VStack {
content
}
.padding()
.background(bubbleColor)
.cornerRadius(12.0)
}
}
struct BubbleView_Previews: PreviewProvider {
static var previews: some View {
BubbleView(bubbleColor: .blue) {
Text("Hello world")
}
}
}

Passing views, or higher-order components, in SwiftUI

As someone who knows React, coming to SwiftUI I'm having challenges to find the right abstractions. Here's an example, but my question is more general. It's related to passing views or, what the React community calls, higher-order components. My example is below. TLDR: how do I abstract and remove duplication in the list views below?
Some models (these will differ in the end):
struct Apple: Comparable, Identifiable {
let id: UUID = UUID()
let label: String
static func < (lhs: Apple, rhs: Apple) -> Bool {
lhs.label < rhs.label
}
}
struct Banana: Comparable, Identifiable {
let id: UUID = UUID()
let label: String
static func < (lhs: Banana, rhs: Banana) -> Bool {
lhs.label < rhs.label
}
}
Some basic detail views (these will differ in the end):
struct AppleView: View {
let apple: Apple
var body: some View {
Text(apple.label)
}
}
struct BananaView: View {
let banana: Banana
var body: some View {
Text(banana.label)
}
}
And two list views with a lot of duplication:
struct AppleListView: View {
let title: String
let apples: [Apple]
var body: some View {
List(apples.sorted()) { apple in
NavigationLink(destination: AppleView(apple: apple)) {
Text(apple.label)
.padding(.all)
}
}
.navigationBarTitle(Text(title), displayMode: .inline)
}
}
struct BananaListView: View {
let title: String
let bananas: [Banana]
var body: some View {
List(bananas.sorted()) { banana in
NavigationLink(destination: BananaView(banana: banana))
Text(banana.label)
.padding(.all)
}
}
.navigationBarTitle(Text(title), displayMode: .inline)
}
}
As you can see, it differs only in small parts. The type of the collection differs and the destination view. I want to remain flexible when it comes to this destination view as Apple and Banana, and their detail views above, will differ in the end. Furthermore it's likely that I want to add Cherry later on, so there's value in abstracting this list view.
So, my question is: how can I best abstract the list views above and remove the duplication in there? What would you suggest? My attempts are below, but it leaves me with type errors. It touches on the higher-order component idea, mentioned earlier.
My attempt with type errors:
struct AppleListView: View {
let title: String
let apples: [Apple]
var body: some View {
ListView(
title: title,
rows: apples, // it complains about types here -> `Cannot convert value of type '[Apple]' to expected argument type 'Array<_>'`
rowView: { apple in Text(apple.label) },
destinationView: { apple in AppleView(apple: apple) }
)
}
}
struct BananaListView: View {
let title: String
let bananas: [Banana]
var body: some View {
ListView(
title: title,
rows: bananas, // it complains about types here -> `Cannot convert value of type '[Banana]' to expected argument type 'Array<_>'`
rowView: { banana in Text(banana.label) },
destinationView: { banana in BananaView(banana: banana) }
)
}
}
struct ListView<Content: View, Row: Comparable & Identifiable>: View {
let title: String
let rows: [Row]
let rowView: (Row) -> Content
let destinationView: (Row) -> Content
var body: some View {
List(rows.sorted()) { row in
NavigationLink(destination: self.destinationView(row)) {
self.rowView(row)
.padding(.all)
}
}
.navigationBarTitle(Text(title), displayMode: .inline)
}
}
It is because you made same type for Label and Destination, here is fixed variant
struct ListView<Target: View, Label: View, Row: Comparable & Identifiable>: View {
let title: String
let rows: [Row]
let rowView: (Row) -> Label
let destinationView: (Row) -> Target
var body: some View {
List(rows.sorted()) { row in
NavigationLink(destination: self.destinationView(row)) {
self.rowView(row)
.padding(.all)
}
}
.navigationBarTitle(Text(title), displayMode: .inline)
}
}

How can I bind static string?

For example, I have this interface:
import SwiftUI
struct ContentView: View {
#Binding var statictext : String
var body: some View {
Text("My New Text: \(statictext)")
}
}
and this class:
class Strings
{
public static var mytext = "MyText"
}
How can I initialize ContentView? I need, than text in interface become "My New Text: new text" when I change the value of Strings.mytext = "new text"
P.S.: sorry for my English)))))
I dont know if this is the right answer for this question but you could do this, even thou it feels kinda dirty to me.
import SwiftUI
class Strings
{
public static var mytext = "MyText"
}
struct ContentView: View {
#State var statictext : String {
willSet {
Strings.mytext = newValue
}
}
init() {
self._statictext = State(initialValue: Strings.mytext)
}
var body: some View {
VStack {
Text("My New Text: \(statictext)")
Button(action: {
self.statictext = "Test"
}) {
Text("Button")
}
}
}
}

Resources