Transform whitespace to + as a result of user input in Swift - swift

I am almost finished with my GitHub Repo Search app. I would need that if users' input contained whitespace, it would be changed to + instead (as GitHub's original browser does while changing the input to the part of the URL).
How to do this?
The code is quite complicated, has many dependancies and is scattered in several files, so if possible I don't want to copy all the passages.
From the frontend side, input is handled like this:
TextField("Enter search", text: $viewModel.query)
where viewModel is a variable that represents struct having a function that allows the screens to change based on the query.
Query itself is a Published var in that struct (Content View Model: Observable Object)
#Published var query = ""
If you need any more information, please let me know in comments. I can copy whole passages as I said but I don't know if it wouldn't complicate understanding the case further ;P

All right, so #jnpdx and #LeoDabus were right and the easiest way in my case was to just transform the String.
I am posting the passage, that I made the transformation in (it has a pointing comment in the line):
import Foundation
import Combine
(...)
private func createRequest(with query: String) -> AnyPublisher<[Item], Error> {
let fixedQuery = query.replacingOccurrences(of: " ", with: "+") //the transformation command
guard let url = URL(string: "https://api.github.com/search/repositories?q=\(fixedQuery)&per_page=20"), !query.isEmpty else {
return Just<[Item]>([])
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
I also add an URL to the more elaborative answer I have found:
Any way to replace characters on Swift String?

Related

Build Recursive Text View in SwiftUI

My goal is to create a SwiftUI view that takes a String and automatically formats that text into Text views. The portion of the string that needs formatting is found using regex and then returned as a Range<String.Index>. This can be used to reconstruct the String once the formatting has been applied to the appropriate Text views. Since there could be multiple instances of text that needs to be formatted, running the formatting function should be done recursively.
struct AttributedText: View {
#State var text: String
var body: some View {
AttributedTextView(text: text)
}
#ViewBuilder
private func AttributedTextView(text: String) -> some View {
if let range = text.range(of: "[0-9]+d[0-9]+", options: .regularExpression) {
//The unattributed text
Text(text[text.startIndex..<range.lowerBound]) +
//Append the attributed text
Text(text[range]).bold() +
//Search for additional instances of text that needs attribution
AttributedTextView(text: String(text[range.upperBound..<text.endIndex]))
} else {
//If the searched text is not found, add the rest of the string to the end
Text(text)
}
}
I get an error Cannot convert value of type 'some View' to expected argument type 'Text', with the recommended fix being to update the recursive line to AttributedTextView(text: String(text[range.upperBound..<text.endIndex])) as! Text. I apply this fix, but still see the same compiler error with the same suggested fix.
A few workarounds that I've tried:
Changing the return type from some View to Text. This creates a different error Cannot convert value of type '_ConditionalContent<Text, Text>' to specified type 'Text'. I didn't really explore this further, as it does make sense that the return value is reliant on that conditional.
Returning a Group rather than a Text, which causes additional errors throughout the SwiftUI file
Neither of these solutions feel very "Swifty". What is another way to go about this? Am I misunderstanding something in SwiftUI?
There are a few things to clarify here:
The + overload of Text only works between Texts which is why it's saying it cannot convert some View (your return type) to Text. Text + Text == Text, Text + some View == ☠️
Changing the return type to Text doesn't work for you because you're using #ViewBuilder, remove #ViewBuilder and it'll work fine.
Why? #ViewBuilder allows SwiftUI to defer evaluation of the closure until later but ensures it'll result in a specific view type (not AnyView). In the case where your closure returns either a Text or an Image this is handy but in your case where it always results in Text there's no need, #ViewBuilder forces the return type to be ConditionalContent<Text, Text> so that it could have different types.
Here's what should work:
private static func attributedTextView(text: String) -> Text {
if let range = text.range(of: "[0-9]+d[0-9]+", options: .regularExpression) {
//The unattributed text
return Text(text[text.startIndex..<range.lowerBound]) +
//Append the attributed text
Text(text[range]).bold() +
//Search for additional instances of text that needs attribution
AttributedTextView(text: String(text[range.upperBound..<text.endIndex]))
} else {
//If the searched text is not found, add the rest of the string to the end
return Text(text)
}
}
I made it static too because there's no state here it's a pure function and lowercased it so it was clear it was a function not a type (the function name looks like a View type).
You'd just call it Self.attributedTextView(text: ...)

Is there a faster method to find words beginning with string inside a string?

I have a field called keywords on Core Data that stores keywords separated by spaces, like:
car nascar race daytona crash
I have a list populated by core data. Every element on that list has keywords.
I have a search field on that view.
I want that list to be filtered as the user types.
If the user types c the app will check elements that have keywords beginning with c. In that case, the element mentioned above will be shown because it has car and crash, both beginning with c.
In order to check that, I created this extension
extension String {
func containsWordStartingWith(insensitive searchWord: String) -> Bool {
let lowercaseSelf = self.lowercased().trimmingCharacters(in: .whitespaces)
let lowercaseSearch = searchWord.lowercased().trimmingCharacters(in: .whitespaces)
let array = lowercaseSelf.components(separatedBy: " ")
return array.contains(where: {$0.hasPrefix(lowercaseSearch)})
}
}
This works but is slow as hell and typing characters on the search bar makes the app stall.
How can I improve that with something faster?
First thing I would do is split the single keywords string into a Set of actual keywords. If possible you should even store it in Core Data that way, so there's no need for a split step.
let keywords = "car nascar race daytona crash"
let keywordSet = Set(keywords.split(separator: " "))
Now the utility method you want is trivial and fast:
func keywordSet(_ set : Set<Substring>, containsWordStartingWith s: Substring) -> Bool {
for keyword in set {
if keyword.hasPrefix(s) { return true }
}
return false
}
Testing:
keywordSet(keywordSet, containsWordStartingWith:"c")

Swift 4+: Issues copying a String to Clipboard using NSPasteboard

I had this all working in Swift 3 and earlier but with Swift 4 no matter what variation I use this code will instead output text as a URL. If I put in "This is my sample text" the output after pasting the clipboard will be "This%20is%20my%20sample%20text". I have tried KuTTypeFileURL but that doesn't appear to make any difference either. What am I missing here? I have seen posts and discussions about how Apple is changing Pboards and other issues with sandboxing but I can't seem to figure this out at all.
original code what was working in swift 3 and earlier
private func copyToClipBoard(textToCopy: String) {
let pasteBoard = NSPasteboard.general()
pasteBoard.clearContents()
pasteBoard.setString(textToCopy, forType: NSStringPboardType)
}
This gives an error of
'NSStringPboardType' is unavailable in Swift: use 'PasteboardType.string'
After searching online I came across these posts that describe the same issue and the workaround was to use the kuTTypeUrl as String
Found here stackoverflow.com/questions/44537356/… and here forums.developer.apple.com/thread/79144
When I try it this way it simply outputs as a URL when I just need a String.
#IBOutlet weak var nameTextField: NSTextField!
#IBAction func nameCopy(_ sender: Any) {
copyToClipBoard(textToCopy: nameTextField.stringValue)
}
let NSStringPboardType = NSPasteboard.PasteboardType(kUTTypeURL as String)
private func copyToClipBoard(textToCopy: String) {
let pasteBoard = NSPasteboard.general
pasteBoard.clearContents()
pasteBoard.setString(textToCopy, forType: NSStringPboardType)
}
You are pasting an URL because you created a PasteboardType kUTTypeURL.
The solution is much simpler, there is a predefined string type
private func copyToClipBoard(textToCopy: String) {
let pasteBoard = NSPasteboard.general
pasteBoard.clearContents()
pasteBoard.setString(textToCopy, forType: .string)
}
The note in the documentation
Apps that adopt App Sandbox cannot access files identified using the string pasteboard type. Instead, use an NSURL object, a bookmark, or a filename pasteboard type.
is related to files (aka string paths), not to regular strings
I just ran into a similar issue. My code looked like this:
NSPasteboard.general.setString("Hello World", forType: .string)
Unfortunately, this didn't work. But I figured there is a bug that if you don't store the NSPasteboard.general into a variable, the object created as part of the general computed property gets deinitialized before the setString change is propagated to the system.
So if you tried doing this in one line like me, just split it up to two instead, which worked for me:
let pasteboard = NSPasteboard.general
pasteboard.setString("Hello World", forType: .string)
I reported this bug via Feedback Assistant to Apple (FB9988062).
UPDATE:
Apple answered my bug report, stating that you need to call declareTypes before setting a value, like so:
NSPasteboard.general.declareTypes([.string], owner: nil)

Parse an URL-path based rest API

I'm create a client app using a REST API. This one use a URL-path format, i.e /api/subPath/{variable}/otherSubPath
I know Apple gives a URLComponents class but seems to work very well only for URL-query-argument i.e /api/path?param=value
I would like to create a class URLBuilder for giving me the different API url dynamically. For the moment I ended up with a class who looks like this:
class URLBuilder {
fileprivate static let base = "https://theAPI.com/"
fileprivate static let objectsPath = kBase + "objects/"
static func informationOfObject(withID id: Int) {
return objectsPath + "\(id)/" + "information/"
}
// Many other functions like this
}
So I would like to know how to get something more elegant (if possible) and maybe use URL-path format with URLComponents.
Or maybe should I use regex? I've never use it but maybe it's useful here.
Have you looked into using the append... family of functions from URL?
appendPathComponent(_:)
appendPathComponent(_:isDirectory:)
appendingPathComponent(_:)
appendingPathComponent_:isDirectory:)
If I understand your requirements, you can do something like:
let baseURL = URL(string: "/api/subPath")!
let variableURL = baseURL.appendingPathComponent("variable")
let trailingURL = variableURL.appendingPathComponent("otherSubPath")
print(trailingURL.absoluteString)
Output: api/subPath/variable/otherSubPath
You could even chain the appending calls together if you need it on a single line.

Function returning specified values but structure doesn't append its values

https://github.com/mateo951/ISBN-Vista-Jera-rquica- Github Link
The structure I have is supposed to be appending values after an internet search. The internet search is called within a function and returns two strings and an image. When I try to append the returned values in the structure, the image is saved but strings are nil.
var datosLibros = [bookData]()
#IBAction func Search(sender: UITextField) {
let (title1, author1, cover1) = (internetSearch(sender.text!))
let libro = bookData(title: title1, author: author1,image:cover1)
datosLibros.append(libro)
print(datosLibros)
}
The saved structured that is printed to the console is the following:
bookData(title: "", author: "", image: <UIImage: 0x7f851a57fbf0>, {0, 0})
Structure:
struct bookData {
var title: String
var author: String
var image: UIImage
init(title: String, author: String, image: UIImage) {
self.title = title
self.author = author
self.image = image
}
}
Thanks in advanced for any advice of help provided. I'm new to swift so there are a lot of stuff uncovered.
The problem is not with the code you posted but with internetSearch.
But before I explain what is going on there, just a quick note about Swift structs. Structs come with one free initializer that takes as its parameters one value for each stored property defined on the struct. Argument labels correspond to the variable labels.
So for your struct bookData (which really should be BookData since types should be capitalized), you do not need to include that initializer you wrote because it will be automatically provided for you as long as you do not create any additional BookData initializers.
Now for the reason your results are not what you expect. Your Strings are not coming back as nil. Instead, they are coming back as empty Strings, or "". In Swift, "" is very different from nil, which means a complete absence of a value. So your Strings are indeed there, they are just empty.
Okay, our Strings are coming back empty. How about our image? No, our image is not coming back either. You thought it was because you saw a UIImage reference printed in the console, but if you look closer you will notice it is a bogus image. Notice "{0, 0}" after the memory address for the instance. As far as I'm aware, this means the image has a size of 0 x 0. How many useful images do you know that have a size of 0 x 0?
So now we have discovered that our Strings are coming back empty and effectively so is our image. What is going on here?
Well, in your implementation of internetSearch I found on GitHub, this is the first thing you do:
var bookTitle = String()
var bookAuthor = String()
var bookCover = UIImage()
Naturally, you do this so that you have some variables of the correct types ready to plop in some actual results if you find them. Just for fun, let's see what the result of the code above would be if there were no results.
Well, the initializer for String that accepts no parameters results in an empty String being created.
Okay, how about our image. While the documentation for UIImage does not even mention an initializer that takes no parameters, it does inherit one from NSObject and it turns out that it will just create an empty image object.
So we now have discovered that what internetSearch is returning is actually the same as what it would be if there were no results. Assuming you are searching for something that you know exists, there must be a problem with the search logic, right? Not necessarily. I noticed that your implementation of the rest of internetSearch relies on an NSURLSession that you use like so:
var bookTitle = String()
var bookAuthor = String()
var bookCover = UIImage()
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(url) { (data, response, error) -> Void in
// Lots of code that eventually sets the three variables above to a found result
}
task.resume()
return (bookTitle, bookAuthor, bookCover)
That seems fine and dandy, except for the fact that NSURLSession performs its tasks asynchronously! Yes, in parts you even dispatch back to the main queue to perform some tasks, but the closure as a whole is asynchronous. This means that as soon as you call task.resume(), NSURLSession executes that task on its own thread/queue/network and as soon as that task is set up it returns way before it completes. So task.resume() returns almost immediately, before any of your search code in the task actually runs, and especially before it completes.
The runtime then goes to the next line and returns those three variables, just like you told it to. This, of course, is the problem because your internetSearch function is returning those initial empty variables before task has a chance to run asynchronously and set them to helpful values.
Suggesting a fully-functional solution is probably beyond the scope of this already-long answer, but it will require a big change in your implementation detail and you should search around for using data returned by NSURLSession.
One possible solution, without me posting any code, is to have your internetSearch function not return anything, but on completion of the task call a function that would then append the result to an array and print it out, like you show. Please research this concept.
Also, I recommend changing your implementation of internetSearch further by declaring your initial values not as:
var bookTitle = String()
var bookAuthor = String()
var bookCover = UIImage()
…but as:
var bookTitle: String?
var bookAuthor: String?
var bookCover: UIImage?
This way, if you find a result than you can represent it wrapped in an Optional and if not you can represent that as nil, which will automatically be the default value of the variables in the code directly above.