I need to highlight a string with certain keys. The base string may contain one or more occurrences of a key.
Test 1:
I use code based on this web site .
import SwiftUI
struct HighlightView: View {
let baseText : String = "President's nearly $2 trillion proposed healthcare, climate, and social spending plan has been floundering for over a month since Mr. Williams came out against the bill in late December. That left the party with little to show after six months of negotiations with the conservative Democratic holdout.\n\nHe later left the door cracked open to future negotiations on a separate plan. Mr. Williams has spoken favorably about some chunks of the bill, including a program to establish universal pre-K and climate spending measures.\n\nMr. Williams wields outsized influence over the Democratic Party's agenda since the Senate is split 50-50 between both parties. Senate Democrats can't pass the package without his support, leaving them with no option but to eventually tailor a separate bill that addresses his concerns about its possible impact on the national debt and inflation.\n\nMr. Williams, meanwhile, appears to be focusing most of his time and energy leading a bipartisan group of senators working on election reform proposals including modernizing and updating the Electoral Count Act of 1887."
let keys = ["Democratic", "Williams", "about"]
var body: some View {
ZStack {
Text(baseText) { str in
for i in 0..<keys.count {
let key = keys[i]
if let range = str.range(of: key) {
str[range].foregroundColor = .pink
}
}
}.foregroundColor(Color.gray).font(.system(size: 18.0))
}
}
}
extension Text {
init(_ string: String, configure: ((inout AttributedString) -> Void)) {
var attributedString = AttributedString(string) /// create an `AttributedString`
configure(&attributedString) /// configure using the closure
self.init(attributedString) /// initialize a `Text`
}
}
The following screenshot shows the result. It handles multiple keys. But it highlights one occurrence for each key.
Test 2: I use code based on some web site, which I'm not permitted to show according to this system.
import SwiftUI
struct HighlightView2: View {
let baseText : String = "President's nearly $2 trillion proposed healthcare, climate, and social spending plan has been floundering for over a month since Mr. Williams came out against the bill in late December. That left the party with little to show after six months of negotiations with the conservative Democratic holdout.\n\nHe later left the door cracked open to future negotiations on a separate plan. Mr. Williams has spoken favorably about some chunks of the bill, including a program to establish universal pre-K and climate spending measures.\n\nMr. Williams wields outsized influence over the Democratic Party's agenda since the Senate is split 50-50 between both parties. Senate Democrats can't pass the package without his support, leaving them with no option but to eventually tailor a separate bill that addresses his concerns about its possible impact on the national debt and inflation.\n\nMr. Williams, meanwhile, appears to be focusing most of his time and energy leading a bipartisan group of senators working on election reform proposals including modernizing and updating the Electoral Count Act of 1887."
let keys = ["Democratic", "Williams", "about"]
var body: some View {
ZStack {
ForEach(keys, id: \.self) { key in
highlightedText(baseText: baseText, match: key, highlightColor: Color.orange)
.foregroundColor(Color.gray).font(.system(size: 19.0))
}
}
}
func highlightedText(baseText: String, match: String, highlightColor: Color) -> Text {
guard !baseText.isEmpty && !match.isEmpty else { return Text(baseText) }
var result: Text!
let components = baseText.components(separatedBy: match)
for i in components.indices {
result = (result == nil ? Text(components[i]) : result + Text(components[i]))
if i != components.count - 1 {
result = result + Text(match).foregroundColor(highlightColor)
}
}
return result ?? Text(baseText)
}
}
This one handles multiple occurrences. But I can only use one key at a time. The following screenshot shows the result.
Do you have a better idea in highlighting a string with multiple keys? Muchos thankos for reading.
Post
Replies
Boosts
Views
Activity
I'm playing with Picker and have come up with some unexpected results. As shown below, I have three pickers in a form.
struct PickColorView: View {
@State var numberIndex: Int = 4
@State var textSizeIndex: Int = 0
var body: some View {
NavigationView {
Form {
Picker(selection: $numberIndex, label: Text("Numbers")) {
ForEach((0...20), id: \.self) {
Text("\($0)")
}
}.onChange(of: numberIndex) { newIndex in
print("Index: \(newIndex)")
}
Picker(selection: $textSizeIndex, label: Text("textSizeTitle")) {
ForEach([14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60], id: \.self) { textSize in
Text("\(textSize)")
}
}.onChange(of: textSizeIndex) { newIndex in
print("Index: \(newIndex)")
}
}
.navigationBarTitle("Settings")
.navigationBarHidden(false)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
Well, if I run it, the top picker shows its initial selection (4) while the other doesn't. I wonder why? The following is a screenshot. (Please ignore the top picker appearing in the screenshot).
If I go ahead and select one with the bottom picker, I end up with an actual value instead of the index. So if I select 18 (Please see the screenshot below.), I expect to get 2 with the onChange thing. But I get 18, instead. So how can I receive the index?
Muchos thankos.
In the following lines of code, I tap a button and an alert message will appear. I wonder if I can change the code such that the alert message will appear after some time, say, 5 seconds? During this period of delay, I want to determine whether or not the app should show an alert message.
import SwiftUI
import Combine
struct ContentView: View {
@State var cancellables = Set<AnyCancellable>()
@State var disabled: Bool = false
@State private var showingAlert = false
let timeInterval: Double = 5.0
var body: some View {
VStack {
Spacer()
Button("Tap me") {
showingAlert = true
disabled = true
// some Combine work //
}
.font(.system(size: 24.0))
.disabled(disabled)
.alert("GGGG", isPresented: $showingAlert) {
/* showing alert after timeInterval */
}
}
}
}
I could do it in UIKit, but I'm not sure if that's possible in SwiftUI.
Muchos thankos.
I've been using Combine with UIKit and Cocoa. The following is a simple example.
import UIKit
import Combine
class ViewController: UIViewController {
// MARK: - Variables
private var cancellableSet: Set<AnyCancellable> = []
@Published var loginText: String = ""
@Published var passwordText: String = ""
// MARK: - IBOutlet
@IBOutlet weak var loginField: UITextField!
@IBOutlet weak var passwordField: UITextField!
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: loginField)
.sink { result in
if let textField = result.object as? UITextField {
if let text = textField.text {
self.loginText = text
}
}
}
.store(in: &cancellableSet)
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: passwordField)
.sink { result in
if let textField = result.object as? UITextField {
if let text = textField.text {
self.passwordText = text
}
}
}
.store(in: &cancellableSet)
Publishers.CombineLatest($loginText, $passwordText)
.sink { (result0, result1) in
if result0.count > 3 && result1.count > 3 {
print("You are good")
} else {
print("No way!!!")
}
}
.store(in: &cancellableSet)
}
}
Now, I want to use Combine with SwiftUI. The following is SwiftUI equivalent, so far.
import SwiftUI
import Combine
struct ContentView: View {
@State var anycancellables = Set<AnyCancellable>()
@State var userText: String = ""
@State var passText: String = ""
@State var canSave: Bool = false
var body: some View {
ZStack {
VStack {
Color.white
}.onTapGesture {
UIApplication.shared.endEditing()
}
VStack {
TextField("Username", text: $userText) {
}.onChange(of: userText) { newValue in
}
SecureField("Password", text: $passText) {
}.onChange(of: passText) { newValue in
}
Spacer()
.frame(height: 20.0)
Button("Save") {
print("Saved...")
}
.foregroundColor(canSave ? Color.black : Color.gray)
.font(.system(size: 32.0))
.disabled(!canSave)
}.padding(.horizontal, 20.0)
}
}
}
So where does Combine fit into the code? I want to enable the Save button if text counts of loginText and passwordText are both greater than 3, which is done at the top with UIKit.
Muchos thankos.
I have a list with five rows as follows.
import SwiftUI
struct ContentView: View {
@State var horizonModels = [MenuModel]()
var body: some View {
ZStack {
Color.green
NavigationView {
List {
ForEach(horizonModels, id: \.self) { model in
if model.id == 0 {
NavigationLink(
destination: MenuView0(),
label: {
Text("\(model.name)")
})
} else {
NavigationLink(
destination: MenuView1(),
label: {
Text("\(model.name)")
})
}
}
}
}
}
.ignoresSafeArea()
.onAppear(perform: {
horizonModels = [
MenuModel.init(id: 0, name: "Shopping"),
MenuModel.init(id: 1, name: "Gift cards"),
MenuModel.init(id: 2, name: "Video"),
MenuModel.init(id: 3, name: "Music"),
MenuModel.init(id: 4, name: "Account")
]
})
}
}
struct MenuModel: Hashable, Identifiable {
let id: Int
let name: String
}
struct MenuView0: View {
var body: some View {
Text("Menu 0")
}
}
struct MenuView1: View {
var body: some View {
Text("Menu 1")
}
}
And I get something like the following screenshot.
Well, I actually want to create a horizontal list like the screenshot below. I could do it except that I am not able to use NavigationView and NavigationLink instead of ScrollView and HStack. So how can I make a horizontally-scrollable list with NavigationView and NavigationLink?
Muchos thankos.
I could do it with completionHandler, but I'm trying to get server data with Combine. The following is what I have.
// UIViewController //
import UIKit
import Combine
class ViewController: UIViewController {
// MARK: - Variables
private var cancellableSet: Set<AnyCancellable> = []
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
let urlStr = "https://api.github.com/repos/ReactiveX/RxSwift/events"
let viewModel = ViewModel(urlStr: urlStr, waitTime: 2.0)
viewModel.fetchData(urlText: viewModel.urlStr, timeInterval: viewModel.waitTime)
.sink { completion in
print("complete")
} receiveValue: { dataSet in
print("count: \(dataSet)")
}
.store(in: &cancellableSet)
print("Yeah...")
}
}
struct DataModel: Hashable, Decodable {
let id: String
let type: String
}
// ViewModel //
import UIKit
import Combine
class ViewModel: NSObject {
var cancellables = [AnyCancellable]()
var urlStr: String
var waitTime: Double
init(urlStr: String, waitTime: Double) {
self.urlStr = urlStr
self.waitTime = waitTime
}
func fetchData(urlText: String, timeInterval: Double) -> Future<[DataModel], Error> {
return Future<[DataModel], Error> { [weak self] promise in
guard let strongSelf = self else { return }
if let url = URL(string: urlText) {
var request = URLRequest(url: url)
request.timeoutInterval = timeInterval
let sessionConfiguration = URLSessionConfiguration.default
let publisher = URLSession(configuration: sessionConfiguration).dataTaskPublisher(for: request)
publisher.sink { completion in
print("complete")
} receiveValue: { (data: Data, response: URLResponse) in
do {
let dataModels = try JSONDecoder().decode([DataModel].self, from: data)
promise(.success(dataModels))
} catch {
print("Error while parsing: \(error)")
promise(.failure("Failure" as! Error))
}
}
.store(in: &strongSelf.cancellables)
} else {
promise(.failure("Failure" as! Error))
}
}
}
}
If I run it, I don't get an error. The app doesn't crash, either. The view controller doesn't deliver anything. What am I doing wrong? Muchos thankos.
I have a toolbar (NSToolbar) through a window controller (NSWindowController). I don't know which Xcode version does it, but the toolbar shows the name of the application next to the three window buttons. Can we hide the name? Old desktop applications that I developed several years ago don't show the name. I hope I can hide it.
Muchos thankos.
If I want to subscribe to four @Published variables at the same time, I can do something like the following.
Publishers.CombineLatest4($variable0, $variable1, $variable2, $variable3)
I wonder if there is any solution to subscribing to more than four variables at the same time?
Muchos thankos
I'm trying to figure out how to use URLSession with the Combine framework.
I have a class that is to fetch data as follows.
import UIKit
import Combine
class APIClient: NSObject {
var cancellables = [AnyCancellable]()
@Published var models = [MyModel]()
func fetchData(urlStr: String) -> AnyPublisher<[MyModel], Never> {
guard let url = URL(string: urlStr) else {
let subject = CurrentValueSubject<[MyModel], Never>([])
return subject.eraseToAnyPublisher()
}
let subject = CurrentValueSubject<[MyModel], Never>(models)
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: [MyModel].self, decoder: JSONDecoder())
.replaceError(with: [])
.sink { posts in
print("api client: \(posts.count)")
self.models = posts
}
.store(in: &cancellables)
return subject.eraseToAnyPublisher()
}
}
I then have a view model class that is to deliver data for my view controller as follows.
import Foundation
import Combine
class ViewModel: NSObject {
@IBOutlet var apiClient: APIClient!
var cancellables = Set<AnyCancellable>()
@Published var dataModels = [MyModel]()
func getGitData() -> AnyPublisher<[MyModel], Never> {
let urlStr = "https://api.github.com/repos/ReactiveX/RxSwift/events"
let subject = CurrentValueSubject<[MyModel], Never>(dataModels)
apiClient.fetchData(urlStr: urlStr)
.sink { result in
print("view model: \(result.count)")
self.dataModels = result
}.store(in: &cancellables)
return subject.eraseToAnyPublisher()
}
}
My view controller has an IBOutlet of ViewModel.
import UIKit
import Combine
class ViewController: UIViewController {
// MARK: - Variables
var cancellables = [AnyCancellable]()
@IBOutlet var viewModel: ViewModel!
// MARK: - IBOutlet
@IBOutlet weak var tableView: UITableView!
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
viewModel.getGitData()
.sink { posts in
print("view controller: \(posts.count)")
}
.store(in: &cancellables)
}
}
If I run it, it seems that ViewModel returns 0 without waiting for APIClient to return data. And the view controller doesn't wait, either. What am I doing wrong? Can I do it without using the completion handler?
In case you need to know what MyModel is, it's a simple struct.
struct MyModel: Decodable {
let id: String
let type: String
}
Muchos thanks
I have the following lines of code to subscribe text changes over two text fields.
import UIKit
import Combine
class ViewController: UIViewController {
var cancellables = Set<AnyCancellable>()
@Published var userText: String = ""
@Published var passText: String = ""
// MARK: - IBOutlet
@IBOutlet var usernameTextField: UITextField!
@IBOutlet var passwordTextField: UITextField!
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: usernameTextField)
.sink(receiveValue: { (result) in
if let myField = result.object as? UITextField {
if let text = myField.text {
self.userText = text
}
}
})
.store(in: &cancellables)
NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: passwordTextField)
.sink(receiveValue: { (result) in
if let myField = result.object as? UITextField {
if let text = myField.text {
self.passText = text
}
}
})
.store(in: &cancellables)
$userText
.sink(receiveValue: { text in
print(text)
})
.store(in: &cancellables)
}
}
In the last several lines, I am printing the text change for userText. Does Combine allow me to observe two variables (userText, passText) at the same time so that I can plug them into a function? If yes, how?
Muchos Thankos.
I'm still a beginner in using Combine. I practice it on and off. Anyway, I have a view model to see changes in two text fields in my view controller as follows.
// ViewModel //
import Foundation
import Combine
class LoginViewModel {
var cancellable = [AnyCancellable]()
init(username: String, password: String) {
myUsername = username
myPassword = password
}
@Published var myUsername: String?
@Published var myPassword: String?
func validateUser() {
print("\(myUsername)")
print("\(myPassword)")
}
}
And my view controller goes as follows.
// ViewController //
import UIKit
import Combine
class HomeViewController: UIViewController {
// MARK: - Variables
var cancellable: AnyCancellable?
// MARK: - IBOutlet
@IBOutlet var usernameTextField: UITextField!
@IBOutlet var passwordTextField: UITextField!
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
cancellable = NotificationCenter.default.publisher(for: UITextField.textDidChangeNotification, object: usernameTextField)
.sink(receiveValue: { result in
if let textField = result.object as? UITextField {
if let text = textField.text {
let loginViewModel = LoginViewModel(username: text, password: "")
loginViewModel.validateUser()
}
}
})
}
}
So I use NSNotification as a publisher to see text changes over one of the text fields. And I cannot see text changes over two of them at the same time. Is there a better approach in seeing text changes over two text fields at the same time using Combine?
Muchos thankos.
Hello,
I'm trying to work out a simple example to fill table view data with Combine. The following is what I have.
import Foundation
struct MyModel: Decodable {
let id: String
let type: String
}
import UIKit
import Combine
class APIClient: NSObject {
var cancellable: AnyCancellable?
let sharedSession = URLSession.shared
func fetchData(urlStr: String, completion: @escaping ([MyModel]?) -> Void) {
guard let url = URL(string: urlStr) else {
return
}
let publisher = sharedSession.dataTaskPublisher(for: url)
cancellable = publisher.sink(receiveCompletion: { (completion) in
switch completion {
case .failure(let error):
print(error)
case .finished:
print("Success")
}
}, receiveValue: { (result) in
let decoder = JSONDecoder()
do {
let post = try decoder.decode([MyModel].self, from: result.data)
completion(post)
} catch let error as NSError {
print("\(error)")
completion(nil)
}
})
}
}
import Foundation
class ViewModel: NSObject {
@IBOutlet var apiClient: APIClient!
var dataModels = [MyModel]()
func getGitData(completion: @escaping () -> Void) {
let urlStr = "https://api.github.com/repos/ReactiveX/RxSwift/events"
apiClient.fetchData(urlStr: urlStr) { (models) in
if let myModels = models {
self.dataModels = myModels.map { $0 }
}
completion()
}
}
}
import UIKit
import Combine
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
// MARK: - Variables
var cancellable: AnyCancellable?
@IBOutlet var viewModel: ViewModel!
@Published var models = [MyModel]()
// MARK: - IBOutlet
@IBOutlet weak var tableView: UITableView!
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
viewModel.getGitData {
self.models = self.viewModel.dataModels
}
cancellable = $models.sink(receiveValue: { (result) in
DispatchQueue.main.async {
[weak self] in
guard let strongSelf = self else { return }
strongSelf.tableView.reloadData()
}
})
}
// MARK: - TableView
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return models.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
let dataModel = models[indexPath.row]
cell?.textLabel?.text = dataModel.id
cell?.detailTextLabel?.text = dataModel.type
return cell!
}
}
I'm not quite comfortable with the lines of code under my view controller (ViewController) in using Combine. How can I make them better? Muchos thankos.
Let me say that I have an IBOutlet object like
@IBOutlet weak var deleteButton: UIButton!
RxCocoa can make this button tap observable like
deleteButton.rx.tap
It doesn't look like Combine lets us observe a button tap. Am I right? I find one approach found at the following URL.
https://www.avanderlee.com/swift/custom-combine-publisher/
And Combine has no native approach? And you still have to use the IBAction?
Hola,
I have the following simple lines of code.
import UIKit
import Combine
class ViewController: UIViewController {
// MARK: - Variables
var cancellable: AnyCancellable?
@Published var labelValue: String?
// MARK: - IBOutlet
@IBOutlet weak var textLabel: UILabel!
// MARK: - IBAction
@IBAction func actionTapped(_ sender: UIButton) {
labelValue = "Jim is missing"
}
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
cancellable = $labelValue
.receive(on: DispatchQueue.main)
.assign(to: \.text, on: textLabel)
}
}
I just wonder what is the point of specifying the main thread with .receive? If I comment out the receive line, the app will still run without a problem. Muchos thankos
import SwiftUI
struct ContentView: View {
@State var numbers = [2021, 9, 30]
var body: some View {
//let firstLocalYear = 2021
let firstLocalMonth = 9
let firstLocalDay = 24
let firstLastDay = 30
NavigationView {
List {
Section(header: Text("Current month")) {
ForEach(firstLocalDay ..< firstLastDay) { i in
HStack {
Text("\(firstLocalMonth)-\(i + 1)")
Spacer()
NavigationLink(
destination: TimeView(numbers: $numbers),
label: {
Text("")
})
}
}
}
}
}
}
}
struct TimeView: View {
@Binding var numbers: [Int]
var body: some View {
HStack {
Text(String(numbers[0]))
Text(String(numbers[1]))
Text(String(numbers[2]))
}
}
}
I have the lines of code above to list some rows of text. For now, numbers is a state variable that is pre-determined. This state variable is passed on to TimeView. Actually, I want to change this array depending on which row the user selects like
numbers = [firstLocalYear, firstLocalMonth, i]
where i comes from the ForEach thing. How can I change this array? Muchos thankos.