Modify view from within a view modifier - swiftui

I have a HighlightedText View which is initialized with a string, a default font and a highlight font. The string can have markers to indicate which portions of it need to be highlighted. That all works fine. Now, instead of initialize it with a default font and a highlight font, I would like to be able to write this in a more swift-ui way to allow for more flexibility and improve readiness. So I would like to be able to do something like this:
HighlightedText("My text <highlighted>")
.defaultFont(.body)
.highlightFont(.title)
I know the standard way should be using a ViewModifier, however all I get from within its body function is a Content type and there doesn't seem to be a way I could cast it into my HighlightedText view and configure it as needed. All I seem to be able to do from within its body is just call other modifiers from View protocol, but that's not enough for my use case.
I've tried this extension, where defaultFont is a file private #State property defined in HighlightedText:
extension HighlightedText {
func defaultFont(_ font: Font) -> some View {
defaultFont.font = font
return body
}
}
That however, does not work. The default font I pass over never gets applied.

Here is possible solution:
if to you have declaration like
struct HighlightedText: View {
var defaultFount = Font.body
var highlightedFont = Font.headline
// ... other code
then your custom modifiers could be as
extension HighlightedText {
func defaultFont(_ font: Font) -> Self {
var view = self
view.defaultFount = font
return view
}
func highlightedFont(_ font: Font) -> Self {
var view = self
view.highlightedFont = font
return view
}
}

Related

ForEach: ID parameter and closure return type

So, I'm going through the SwiftUI documentation to get familiar. I was working on a grid sample app. It has the following code:
ForEach(allColors, id: \.description) { color in
Button {
selectedColor = color
} label: {
RoundedRectangle(cornerRadius: 4.0)
.aspectRatio(1.0, contentMode: ContentMode.fit)
.foregroundColor(color)
}
.buttonStyle(.plain)
}
It didn't occur to me first that ForEach is actually a struct, I thought it's a variation of the for in loop at first so I'm quite new at this. Then I checked the documentation.
When I read the documentation and some google articles for the ForEach struct, I didn't understand two points in the code:
So we are initializing the foreach struct with an array of colors. For the the ID why did they use .\description instead of .self?
Second is using color in. Since foreach is a struct and the paranthesis is the initializtion parameters this looks like the return type of a closure but why would we return individual colors to foreach? I thought the return is a collection of views or controls like button and label. This is like var anInteger: Int = 1 for example. What type does ForEach accept as a result of the closure? Or am I reading this all wrong?
So we are initializing the foreach struct with an array of colors. For the the ID why did they use .\description instead of .self?
It depends on the type of allColors. What you should have in mind that id here is expected to be stable. The documentation states:
It’s important that the id of a data element doesn’t change unless you replace the data element with a new data element that has a new identity. If the id of a data element changes, the content view generated from that data element loses any current state and animations.
So for example if colors are reference types (which are identifiable) and you swap one object with an identical one (in terms of field values), the identity will change, whereas description wouldn't (for the purposes of this example - just assuming intentions of code I have no access to).
Edit: Also note that in this specific example allColors appears to be a list of Color, which is not identifiable. So that's the reason behind the custom id keyPath.
Regarding your second point, note that the trailing closure is also an initialization parameter. To see this clearly we could use the "non-sugared" version:
ForEach(allColors, id: \.description, content: { color in
Button {
selectedColor = color
} label: {
RoundedRectangle(cornerRadius: 4.0)
.aspectRatio(1.0, contentMode: ContentMode.fit)
.foregroundColor(color)
}
.buttonStyle(.plain)
})
where content is a closure (an anonymous function) that gets passed an element of the collection and returns some View.
So the idea is something like this: "Give me an collection of identifiable elements and I will call a function for each of these elements expecting from you to return me some View".
I hope that this makes (some) sense.
Additional remarks regarding some of the comments:
It appears to me that the main source of confusion is the closure itself. So let's try something else. Let's write the same code without a closure:
ForEach's init has this signature:
init(_ data: Data, id: KeyPath<Data.Element, ID>, content: #escaping (Data.Element) -> Content)
Now, the content translates to:
A function with one parameter of type Data.Element, which in our case is inferred from the data so it is a Color. The function's return type is Content which is a view builder that produces some View
so our final code, which is equivalent to the first one, could look like this:
struct MyView: View {
let allColors: [Color] = [.red, .green, .blue]
#State private var selectedColor: Color?
var body: some View {
List {
ForEach(allColors, id: \.description, content: colorView)
}
}
#ViewBuilder
func colorView(color: Color) -> some View {
Button {
selectedColor = color
} label: {
RoundedRectangle(cornerRadius: 4.0)
.aspectRatio(1.0, contentMode: ContentMode.fit)
.foregroundColor(color)
}
.buttonStyle(.plain)
}
}
I hope that this could help to clarify things a little bit better.

How to respond to hover / click event on AttributedString in SwiftUI

I am using AttributedString in SwiftUI (Mac application) to customize the appearance of portions of a long string. I'm displaying the text formatted successfully and it appears correct.
My code looks like this:
struct TextView: View {
var body: some View {
ScrollView {
Text(tag())
}.padding()
}
func tag() -> AttributedString {
// code which creates the attributed string and applies formatting to various locations
}
}
At this point I want to add "touch points" ("interactive points") to the text (imagine hyperlinks) which will provide additional information when particular locations (pieces of text) are interacted with.
Ive seen some similar questions describing usage (or combinations) of NSTextAttachment , NSAttributedStringKey.link , UITextViewDelegate
see:
NSAttributedString click event in UILabel using Swift
but this isn't (or at least not obvious) the idiomatic "SwiftUI" way and seems cumbersome.
I would want to tag the string with the formatting while adding the "Attachment" which can be recognized in the view event handler:
func tag() -> AttributedString {
// loose for this example
var attributedString = AttributedString("My string which is very long")
for range in getRangesOfAttributes {
attributedString[range].foregroundColor = getRandomColor()
attributedString[range].attachment = Attachment() <<<<<<< this is missing, how do I tag this portion and recognize when it got interacted with in the View
}
}
func getRangesOfAttributes() -> ClosedRange<AttributedString.Index> {
... returns a bunch of ranges which need to be tagged
}
// the view can now do something once the attachment is clicked
var body: View {
Text(tag())
.onClickOfAttachment(...) // <<<< This is contrived, how can I do this?
}

SwiftUI Size to fit or word-wrap navigation title

I have a navigation title that is too large on some smaller devices.
I've read many ways to set the titleTextAttributes and largeTitleTextAttributes of the UINavigationBar.appearance() however when setting the paragraph style to word wrap, it seems to remove the standard ... clipping and have the text continue off the edge of the screen without wrapping:
init() {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineBreakMode = .byWordWrapping
UINavigationBar.appearance().largeTitleTextAttributes = [
.paragraphStyle: paragraphStyle
]
}
I want to maintain the SwiftUI behaviour where the title is shown as large text until the view is scrolled up and it moves to the navigation bar, so getting the .toolbar directly won't help.
I also don't want to just specify a smaller font as I only want it to shrink or wrap if necessary.
Has anyone managed to achieve this?
You can add this line on the initializer of the view where yo have the issue
UILabel.appearance(whenContainedInInstancesOf: [UINavigationBar.self]).adjustsFontSizeToFitWidth = true
Example:
struct YourView: View {
init() {
UILabel.appearance(whenContainedInInstancesOf: [UINavigationBar.self]).adjustsFontSizeToFitWidth = true
}
var body: some View {
NavigationView {
Text("Your content")
.navigationBarTitle("Very very large title to fit in screen")
}
}
}

SwiftUI - How to set a non static var from a static function

I'm currently following a series of lectures from Stanford to learn Swift (Code is from lectures 1-4 plus the homework I'm trying to complete) and am having an issue with setting an instance var(non-static) from a static function. I have gotten this to work but I would like it to be able to have different instances of the game have different themes
class EmojiMemoryGame: ObservableObject {
//Create the model in the viewController
#Published private var cardGame = EmojiMemoryGame.createMemoryGame()
static let themesArray = [
theme(name: "Halloween", color: Color.orange, emojiArray: ["πŸ’€","πŸ•·","πŸ‘»","πŸŽƒ","πŸ‘½","🍬","πŸŒ‘"])
{6},
theme(name: "Aqua", color: Color.blue, emojiArray: ["πŸ΄β€β˜ οΈ","🐬","πŸ’§","βš“οΈ","🏝","🐚","⛡️","πŸ¦‘","🐑","🐠"]) {Int.random(in: 5...7)},
theme(name: "Animals", color: Color.gray, emojiArray: ["🦜","🐈","πŸ“","🦝","πŸ‡","πŸ–","🐎","🦌"])
{Int.random(in: 3...4)}
]
static var selectedTheme: theme?
//Public Static Function
//Input: None
//Output: MemoryGame<String> struct
//Usage: Return the card game with emoijs inited
static func createMemoryGame() -> MemoryGame<String> {
//ISSUE
//Want to get a random theme from the themes array and assign it to an instance of this class but get an error beceause the function is static
selectedTheme = themesArray[Int.random(in: 0..<themesArray.count)]
let emojisArray = selectedTheme!.emojiArray
//Create the actual MemoryGame
return MemoryGame<String>(numberOfPairsOfCards: selectedTheme!.numberOfCards(), cardContentFactory: { cardPairIndex in
emojisArray[cardPairIndex]
})
}
//Struct Theme
struct theme {
let name: String
let color: Color
let emojiArray: [String]
let numberOfCards: () -> Int
}
I want to be able to get a random theme from themesArray but let selectedTheme be an instance var. Can anyone give me some advice?
You don't need to declare your selectedTheme as static.
You just need to add an initialiser and change what is returned from the function that creates your memory game. I've removed some of the code to make it easier to see what I have changed.
Notice that we have removed the setting of cardGame to the init. We have also removed all reference to selectedTheme from createMemoryGame. The createMemoryGame function now returns a tuple, which means that we can easily access the game and theme that it has created.
class EmojiMemoryGame: ObservableObject {
#Published private var cardGame: MemoryGame<String>
var selectedTheme: Theme // structs and classes should have capital letters
/// This now returns a tuple of the game we have created and the theme that we have chosen
static func createMemoryGame() -> (MemoryGame<String>, Theme) {
let theme = EmojiMemoryGame.themesArray[Int.random(in: 0..<themesArray.count)]
let emojisArray = theme!.emojiArray
let game = MemoryGame<String>(numberOfPairsOfCards: theme!.numberOfCards(), cardContentFactory: { cardPairIndex in
emojisArray[cardPairIndex]
})
return (game, theme)
}
// We add an initialiser to our class so that all the values that we need are initialised
init() {
let (game, theme) = EmojiMemoryGame.createMemoryGame() // note that the use of the names game and theme are arbitrary.
self.cardGame = game
self.selectedTheme = theme
}
}
Just let your ThemesArray be a static variable like it is. However, you won't need to declare selectedTheme as static. As you say, it is an instance variable.
Declare selectedTheme as following:
var selectedTheme: theme?
Then initialize it like the following:
selectedTheme = EmojiMemoryGame.themesArray[Int.random(in: 0..<EmojiMemoryGame.themesArray.count)]
let emojisArray = selectedTheme!.emojiArray

How to update a complicated model of a View from within its body

I am building a custom View struct with a large and complicated model that I want to update from within the var body : some View { ... } property (ex. tapping on a button representing a table column in the view should change the sort order of the rows in the table). I am not allowed to modify this model from within body because self is immutable however:
struct MyTable : View {
struct Configuration {
// a LOT of data here, ie header, rows, footer, style, etc
mutating func update() { ... } // this method must be called before view render
}
var configuration : Configuration
var body : some View {
// build the view here based on configuration
self.configuration.columns[3].sortAscending = false // error, `self` is immutable
self.configuration.update() // error, `self` is immutable
}
}
I really don't want to create #State variables for all of the configuration data because 1) there's a lot of configuration data, 2) it would be difficult to model the model in such a way.
I tried instead making configuration a #State var, but I am unable to set the configuration object at init() time even though the code compiles and runs! (BTW, the configuration var now needs to be initialized before init, otherwise I get an error on the line self.configuration = c that self is used before all members are initialized -- this is likely a complication with using #State which is a property wrapper.)
struct MyTable : View {
struct Configuration {
...
}
#State var configuration : Configuration = Configuration() // must initialize the #State var
init(configuration c: Configuration) {
self.configuration = c // does not assign `c` to `configuration` !!!
self.$configuration.wrappedValue = c // this also does not assign !!!
self.configuration.update()
}
var body : some View {
// build the view here based on configuration
self.configuration.columns[3].sortAscending = false // ok, no error now about mutating a #State var
self.configuration.update() // ok, no error
}
}
I came up with a work around where I don't need to call update() in MyTable.init() by creating a custom init() in Configuration that will call update(). This way the init() in MyTable is unnecessary and this approach resolves all previously encountered problems:
struct MyTable : View {
struct Configuration {
...
init(...) {
...
self.update()
}
mutating func update() { ... } // this method must be called before view render
}
#State var configuration : Configuration // note that this property can't be marked private because we want to inject `configuration` at init
var body : some View {
// build the view here based on configuration
self.configuration.columns[3].sortAscending = false // ok, no error now about mutating a #State var
self.configuration.update() // ok, no error
...
}
}
Then in my calling code:
return MyTable.init(configuration: MyTable.Configuration.init(...))