SwiftUI: Not getting dropped NSString value in DropDelegate - swiftui

I'm trying to implement a simple drag and drop with NSString. I can get this working with NSURL but not NSString. Every time I try the drag and drop I always end up with a nil value. Here is my code:
struct ContentView: View
{
var body: some View
{
Text("Hello World")
.onDrag { NSItemProvider(object: "Hello World" as NSString) }
}
}
Then the dropped object:
struct DroppedView: View, DropDelegate
{
var body: some View
{
Text("Drop here")
.onDrop(of: ["public.text"], delegate: self)
}
func performDrop(info: DropInfo) -> Bool
{
if let items = info.itemProviders(for: ["public.text"]).first
{
item.loadItem(forTypeIdentifier: "public.text", options: nil)
{
(data, error) in
print(data)
}
}
return true
}
}
I would have expected the output to be "Hello World". What am I missing?

I don't recall where (somewhere in depth of Apple's docs) but I met occasionally that we should not use base UTTypes (like public.text) for data loading - instead only concrete types (like in this answer) must be used.
The NSString register itself as UTF-8 plain text type provider (it conforms to public.text but cannot be decoded to), so worked variant is
func performDrop(info: DropInfo) -> Bool
{
if let item = info.itemProviders(for: ["public.utf8-plain-text"]).first
{
item.loadItem(forTypeIdentifier: "public.utf8-plain-text", options: nil)
{
(data, error) in
if let data = data as? Data {
print(NSString(data: data, encoding: 4) ?? "failed")
}
}
}
return true
}
Tested with Xcode 13 / macOS 11.5.2

Related

nested loadObject call issue

I write some code to show an image dragged from other apps(such as web browser, photos, etc). I make a delegate to perform the drop.
If DropInfo has an image item, I will try to retrieve the data as uiimage by NSItemProvider.loadObject first. If errors occur during loading, I will ask DropInfo again whether it has a url item. If the answer is YES, I will try to retrieve URL by NSItemProvider.loadObject.
It means the second loadObject will be nested in the first one. When running the simulator, I find the second loadObject completion handler is never called which is supposed to be called when I try to retrieve URL. Do I miss anything?
import SwiftUI
import Combine
struct ContentView: View{
#State private var img: UIImage?
var body: some View{
Image(uiImage: img != nil ? img! : UIImage(systemName: "photo")!)
.resizable()
.scaledToFit()
.onDrop(of: [.image], delegate: ImageDropController(img: $img))
}
}
class ImageDropController: DropDelegate{
#Binding var img: UIImage?
init(img: Binding<UIImage?>) {
_img = img
}
func performDrop(info: DropInfo) -> Bool {
if getImgFromImage(info: info){
return true
}else{
return false
}
}
func getImgFromImage(info: DropInfo) -> Bool{
guard info.hasItemsConforming(to: [.image]) else {
return getImgFromUrl(info: info)
}
createImgFromImage(from: info.itemProviders(for: [.image]).first!,info: info)
return true
}
func createImgFromImage(from provider: NSItemProvider, info: DropInfo){
provider.loadObject(ofClass: UIImage.self) { image, error in
var unwrappedImage: UIImage?
if let error = error {
print("unwrapImage failed: ", error.localizedDescription)
_ = self.getImgFromUrl(info: info)
} else {
unwrappedImage = image as? UIImage
}
if let image = unwrappedImage{
DispatchQueue.main.async {
self.img = image
}
}
}
}
func getImgFromUrl(info: DropInfo) -> Bool{
guard info.hasItemsConforming(to: [.url]) else {
return false
}
createImgFromUrl(from: info.itemProviders(for: [.url]).first!)
return true
}
private func createImgFromUrl(from provider: NSItemProvider){
var fetchUrl: URL?
print("create from url")
_ = provider.loadObject(ofClass: URL.self) { url, error in
print("nested handler") <<------- never be called
if let error = error {
print("unwrapUrl failed: ", error.localizedDescription)
} else {
fetchUrl = url
print("url", fetchUrl?.description)
}
if let url = fetchUrl{
// Do some data fetch work using url
}
}
}
}

SwiftUI Search Bar in line with navigation bar

Does anyone have working Swiftui code that will produce a search bar in the navigation bar that is inline with the back button? As if it is a toolbar item.
Currently I have code that will produce a search bar below the navigation back button but would like it in line like the picture attached shows (where the "hi" is):
I am using code that I found in an example:
var body: some View {
let shopList = genShopList(receiptList: allReceipts)
VStack{
}
.navigationBarSearch(self.$searchInput)
}
public extension View {
public func navigationBarSearch(_ searchText: Binding<String>) -> some View {
return overlay(SearchBar(text: searchText)
.frame(width: 0, height: 0))
}
}
fileprivate struct SearchBar: UIViewControllerRepresentable {
#Binding
var text: String
init(text: Binding<String>) {
self._text = text
}
func makeUIViewController(context: Context) -> SearchBarWrapperController {
return SearchBarWrapperController()
}
func updateUIViewController(_ controller: SearchBarWrapperController, context: Context) {
controller.searchController = context.coordinator.searchController
}
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text)
}
class Coordinator: NSObject, UISearchResultsUpdating {
#Binding
var text: String
let searchController: UISearchController
private var subscription: AnyCancellable?
init(text: Binding<String>) {
self._text = text
self.searchController = UISearchController(searchResultsController: nil)
super.init()
searchController.searchResultsUpdater = self
searchController.hidesNavigationBarDuringPresentation = true
searchController.obscuresBackgroundDuringPresentation = false
self.searchController.searchBar.text = self.text
self.subscription = self.text.publisher.sink { _ in
self.searchController.searchBar.text = self.text
}
}
deinit {
self.subscription?.cancel()
}
func updateSearchResults(for searchController: UISearchController) {
guard let text = searchController.searchBar.text else { return }
self.text = text
}
}
class SearchBarWrapperController: UIViewController {
var searchController: UISearchController? {
didSet {
self.parent?.navigationItem.searchController = searchController
}
}
override func viewWillAppear(_ animated: Bool) {
self.parent?.navigationItem.searchController = searchController
}
override func viewDidAppear(_ animated: Bool) {
self.parent?.navigationItem.searchController = searchController
}
}
}
If anyone has a solution to this problem that would be greatly appreciated! I know that in IoS 15 they are bringing out .searchable but looking for something that will work for earlier versions too.
You can put any control in the position you want by using the .toolbar modifier (iOS 14+) and an item with .principal placement, e.g.:
var body: some View {
VStack {
// rest of view
}
.toolbar {
ToolbarItem(placement: .principal) {
MySearchField(text: $searchText)
}
}
}
A couple of things to note:
The principal position overrides an inline navigation title, either when it's set with .navigationBarTitleDisplayMode(.inline) or when you have a large title and scroll up the page.
It's possible that your custom view expands horizontally so much that the back button loses any text component. Here, I used a TextField to illustrate the point:
You might be able to mitigate for that by assigning a maximum width with .frame(maxWidth:), but at the very least it's something to be aware of.

swiftui list doens't appear but array isn't empty

I am working on a Swiftui file that loads data from Firebase.
It did work but when I added things it suddenly stopt working...
I tried to strip it back down but I can't get it working again.
Does anyone know what I do wrong?
import SwiftUI
import Firebase
struct Fav: View {
#StateObject var loader = Loader()
var body: some View {
ScrollView {
if loader.userfav.count != 0 {
List (loader.userfav, id: \.id) { fav in
Text(fav.name.capitalized)
}
}
else
{
Text("You haven't added favorits yet...")
}
}
.onAppear{
loader.loadfav(loadfavorits: "asd")
}
.navigationBarTitle("")
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
}
func deletefav (docid: String) {
print(docid)
}
}
struct Fav_Previews: PreviewProvider {
static var previews: some View {
Fav()
}
}
and the loader file
import Foundation
import Firebase
import FirebaseFirestore
class Loader : ObservableObject {
private var db = Firestore.firestore()
#Published var userfav = [fav]()
func loadfav (loadfavorits: String) {
userfav = [fav]()
db.collection("favo").whereField("user", isEqualTo: loadfavorits).getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting favorits: \(err.localizedDescription)")
}
else
{
for fav in querySnapshot!.documents {
let brand = fav.get("brand") as! String
let store = fav.get("store") as! String
let name = fav.get("name") as! String
let type = fav.get("type") as! String
let docid = fav.get("docid") as! String
self.userfav.append(fav(brand: brand, store: store, name: name, type: type, docid: docid))
}
}
}
}
}
It doesn't show the Text("You haven't added favorits yet...")
So that means dat loader.userfav.count is not empty
Having a List embedded in a ScrollView (which also scrolls) can lead to layout problems. Remove the outer ScrollView and the issue will be solved.

How to Append to Array of Structs and have Change Persistant?

I want to be able to store a list of objects in a separate Swift file and call them in a page to have them show up. I successful did this with this code:
import Foundation
import SwiftUI
struct MatchInfo: Hashable, Codable {
let theType: String
let theWinner: String
let theTime: String
let id = UUID()
}
var matchInfo = [
MatchInfo(theType: "Capitalism", theWinner: "Julia", theTime: "3/3/2021"),
MatchInfo(theType: "Socialism", theWinner: "Julia", theTime: "3/2/2021"),
MatchInfo(theType: "Authoritarianism", theWinner: "Luke", theTime: "3/1/2021")
]
where I append to the list after a match is played on another page here:
matchInfo.insert(MatchInfo(theType: typeSelection, theWinner: winnerName, theTime: "\(datetimeWithoutYear)" + "\(year)"), at: 0)
And heres some of the code on another page where I call it into a list:
List {
ForEach(matchInfo, id: \.self) { matchData in
matchRow(matchData : matchData)
} .background(Color("invisble"))
.listRowBackground(Color("invisble"))
} .frame(height: 490)
...
struct matchRow: View {
let matchData: MatchInfo
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(matchData.theType)
.font(.system(size: 20, weight: .medium, design: .default))
Text(" Winner: " + matchData.theWinner)
}
Spacer()
Text(matchData.theTime)
.padding(.leading, 40)
.multilineTextAlignment(.trailing)
}
.foregroundColor(.white)
.accentColor(.white)
}
}
But this code doesn't save through app restarts. I've never had something save through a restart before and have been struggling to find an answer simple enough for me to understand. How can I update the list without it going away next time I open the app?
Okay so here is an example on how you save to/ load from the documents folder.
First of all make sure that you object MatchInfo conforms to this protocol.
import Foundation
protocol LocalFileStorable: Codable {
static var fileName: String { get }
}
extension LocalFileStorable {
static var localStorageURL: URL {
guard let documentDirectory = FileManager().urls(for: .documentDirectory, in: .userDomainMask).first else {
fatalError("Can NOT access file in Documents.")
}
return documentDirectory
.appendingPathComponent(self.fileName)
.appendingPathExtension("json")
}
}
extension LocalFileStorable {
static func loadFromFile() -> [Self] {
do {
let fileWrapper = try FileWrapper(url: Self.localStorageURL, options: .immediate)
guard let data = fileWrapper.regularFileContents else {
throw NSError()
}
return try JSONDecoder().decode([Self].self, from: data)
} catch _ {
print("Could not load \(Self.self) the model uses an empty collection (NO DATA).")
return []
}
}
}
extension LocalFileStorable {
static func saveToFile(_ collection: [Self]) {
do {
let data = try JSONEncoder().encode(collection)
let jsonFileWrapper = FileWrapper(regularFileWithContents: data)
try jsonFileWrapper.write(to: self.localStorageURL, options: .atomic, originalContentsURL: nil)
} catch _ {
print("Could not save \(Self.self)s to file named: \(self.localStorageURL.description)")
}
}
}
extension Array where Element: LocalFileStorable {
///Saves an array of LocalFileStorables to a file in Documents
func saveToFile() {
Element.saveToFile(self)
}
}
Your main Content View should look like this: (I modified your object to make it a bit simpler.)
import SwiftUI
struct MatchInfo: Hashable, Codable, LocalFileStorable {
static var fileName: String {
return "MatchInfo"
}
let description: String
}
struct ContentView: View {
#State var matchInfos = [MatchInfo]()
var body: some View {
VStack {
Button("Add Match Info:") {
matchInfos.append(MatchInfo(description: "Nr." + matchInfos.count.description))
MatchInfo.saveToFile(matchInfos)
}
List(matchInfos, id: \.self) {
Text($0.description)
}
.onAppear(perform: {
matchInfos = MatchInfo.loadFromFile()
})
}
}
}

SwiftUI: Real device shows strange behavior with asynchronous flow, while Simulator runs perfectly

*** EDIT 23.20.20 ***
Due to the strange behavior discovered after my original post, I need to completely rephrase my question. I meanwhile re-wrote large parts of my code as well.
The issue:
I run an asynchronous HTTP GET search query, which returns me an Array searchResults, which I store in an ObservedObject FoodDatabaseResults.
struct FoodItemEditor: View {
//...
#ObservedObject var foodDatabaseResults = FoodDatabaseResults()
#State private var activeSheet: FoodItemEditorSheets.State?
//...
var body: some View {
NavigationView {
VStack {
Form {
Section {
HStack {
// Name
TextField(titleKey: "Name", text: $draftFoodItem.name)
// Search and Scan buttons
Button(action: {
if draftFoodItem.name.isEmpty {
self.errorMessage = NSLocalizedString("Search term must not be empty", comment: "")
self.showingAlert = true
} else {
performSearch()
}
}) {
Image(systemName: "magnifyingglass").imageScale(.large)
}.buttonStyle(BorderlessButtonStyle())
//...
}
//...
}
//...
}
}
//...
}
.sheet(item: $activeSheet) {
sheetContent($0)
}
}
private func performSearch() {
UserSettings.shared.foodDatabase.search(for: draftFoodItem.name) { result in
switch result {
case .success(let networkSearchResults):
guard let searchResults = networkSearchResults else {
return
}
DispatchQueue.main.async {
self.foodDatabaseResults.searchResults = searchResults
self.activeSheet = .search
}
case .failure(let error):
debugPrint(error)
}
}
}
#ViewBuilder
private func sheetContent(_ state: FoodItemEditorSheets.State) -> some View {
switch state {
case .search:
FoodSearch(foodDatabaseResults: foodDatabaseResults, draftFoodItem: self.draftFoodItem) // <-- I set a breakpoint here
//...
}
}
}
class FoodDatabaseResults: ObservableObject {
#Published var selectedEntry: FoodDatabaseEntry?
#Published var searchResults: [FoodDatabaseEntry]?
}
I get valid search results in my performSearch function. The DispatchQueue.main.async closure makes sure to perform the update of my #Published var searchResults in the main thread.
I then open a sheet, displaying these search results:
struct FoodSearch: View {
#ObservedObject var foodDatabaseResults: FoodDatabaseResults
#Environment(\.presentationMode) var presentation
//...
var body: some View {
NavigationView {
List {
if foodDatabaseResults.searchResults == nil {
Text("No search results (yet)")
} else {
ForEach(foodDatabaseResults.searchResults!) { searchResult in
FoodSearchResultPreview(product: searchResult, isSelected: self.selectedResult == searchResult)
}
}
}
.navigationBarTitle("Food Database Search")
.navigationBarItems(leading: Button(action: {
// Remove search results and close sheet
foodDatabaseResults.searchResults = nil
presentation.wrappedValue.dismiss()
}) {
Text("Cancel")
}, trailing: Button(action: {
if selectedResult == nil {
//...
} else {
//... Do something with the result
// Remove search results and close sheet
foodDatabaseResults.searchResults = nil
presentation.wrappedValue.dismiss()
}
}) {
Text("Select")
})
}
}
}
When I run this on the Simulator, everything works as it should, see https://wolke.rueth.info/index.php/s/KbqETcDtSe4278d
When I run it on a real device with the same iOS version (14.0.1), the FoodSearch view first correctly displays the search result, but is then immediately called a second time with empty (nil) search results. You need to look very closely at the screen cast here and you'll see it displaying the search results for a very short moment before they disappear: https://wolke.rueth.info/index.php/s/9n2DZ88qSB9RWo4
When setting a breakpoint in the marked line in my sheetContent function, the FoodSearch sheet is indeed called twice on the real device, while it's only called once in the Simulator.
I have no idea what is going on here. Hope someone can help. Thanks!
*** ORIGINAL POST ***
I run an HTTP request, which updates a #Published variable searchResults in a DispatchQueue.main.async closure:
class OpenFoodFacts: ObservableObject {
#Published var searchResults = [OpenFoodFactsProduct]()
// ...
func search(for term: String) {
let urlString = "https://\(countrycode)-\(languagecode).openfoodfacts.org/cgi/search.pl?action=process&search_terms=\(term)&sort_by=unique_scans_n&json=true"
let request = prepareRequest(urlString)
let session = URLSession.shared
session.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) in
guard error == nil else {
debugPrint(error!.localizedDescription)
return
}
if let data = data {
do {
let openFoodFactsSearchResult = try JSONDecoder().decode(OpenFoodFactsSearchResult.self, from: data)
guard let products = openFoodFactsSearchResult.products else {
throw FoodDatabaseError.noSearchResults
}
DispatchQueue.main.async {
self.searchResults = products
self.objectWillChange.send()
}
} catch {
debugPrint(error.localizedDescription)
}
}
}).resume()
}
struct OpenFoodFactsSearchResult: Decodable {
var products: [OpenFoodFactsProduct]?
enum CodingKeys: String, CodingKey {
case products
}
}
struct OpenFoodFactsProduct: Decodable, Hashable, Identifiable {
var id = UUID()
// ...
enum CodingKeys: String, CodingKey, CaseIterable {
// ...
}
// ...
}
I call the search function from my view:
struct FoodSearch: View {
#ObservedObject var foodDatabase: OpenFoodFacts
// ...
var body: some View {
NavigationView {
List {
ForEach(foodDatabase.searchResults) { searchResult in
FoodSearchResultPreview(product: searchResult, isSelected: self.selectedResult == searchResult)
}
}
// ...
}
.onAppear(perform: search)
}
private func search() {
foodDatabase.search(for: draftFoodItem.name)
}
}
My ForEach list will never update, although I have a valid searchResult set in my OpenFoodFacts observable object and also sent an objectWillChange signal. Any idea what I'm missing?
Funny enough: On the simulator it works as expected:
https://wolke.rueth.info/index.php/s/oy4Xf6C5cgrEZdK
On a real device not:
https://wolke.rueth.info/index.php/s/TQz8HnFyjLKtN74