On SwiftUI AppKit project, dropEntered of DropDelegate not trigged - swift

The code from https://github.com/onmyway133/blog/issues/594, and make some changes.
It's very simple, create a SwiftUI + Appkit project, copy and paste the code below to the project.
import SwiftUI
struct SelectFileView: View {
let buttonTitle: String
#State var isDrop: Bool = false
var body: some View {
VStack(alignment: .leading) {
Button(action: {}) {
Text(buttonTitle)
}
.offset(x: -16)
Text("Alternatively, you can drag and drop file here")
.font(.footnote)
.foregroundColor(Color.gray)
}
.border(isDrop ? Color.orange : Color.clear)
.onDrop(of: ["public.image"], delegate: self)
.padding(32)
}
}
extension SelectFileView: DropDelegate {
func dropEntered(info: DropInfo) {
print("dropEntered")
self.isDrop = true
}
func dropExited(info: DropInfo) {
print("dropExited")
self.isDrop = false
}
func performDrop(info: DropInfo) -> Bool {
guard
let itemProvider = info.itemProviders(for: ["public.image"]).first
else { return false }
itemProvider.loadItem(forTypeIdentifier: "public.image", options: nil) { item, error in
guard
let data = item as? Data,
let url = URL(dataRepresentation: data, relativeTo: nil)
else { return }
}
return true
}
}
struct ContentView: View {
var body: some View {
SelectFileView(buttonTitle: "Drop Test")
}
}
After building and running on Xcode 12.0 beta 2 (12A6163b), it always print dropExited, and this is the problem.
dropEntered(info:) not trigged, but dropExited(info:) works

Related

Array Indexing occurs Fatal error: Index out of range

Problem
Hello. I'm studying SwiftUI.
I've tried to pick multiple photos from gallery using PHPickerController, and show up multiple views which represents each photo one by one. However, Fatal error occurs whenever I try to access any index of vm.images.
How could I solve this issue?
My source code is as follows
Solved
The problem comes from vm.images I thought that .onChange modifier operate after all images are saved into vm.images. But it didn't.
I solved this matter by adding if statement when calling PickerTabView; Quite Easy
// Added code
if let images = vm.images {
if images.count > 0 {
PickerTabView()
}
}
struct PickerTabView: View {
#EnvironmentObject var vm: ViewModel
var body: some View {
TabView {
if let images = vm.images{
ForEach(images, id: \.self) { image in
PickerSettingView(image: image)
}
}
}
.tabViewStyle(.page)
}
}
struct ImagesPicker: UIViewControllerRepresentable {
#Binding var selectedImages: [UIImage]?
//var selectionLimit: Int
//var filter: PHPickerFilter?
var itemProvider: [NSItemProvider] = []
func makeUIViewController(context: Context) -> some PHPickerViewController {
var configuration = PHPickerConfiguration()
configuration.selectionLimit = 20
configuration.filter = .images
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
func makeCoordinator() -> Coordinator {
return ImagesPicker.Coordinator(parent: self)
}
class Coordinator: NSObject, PHPickerViewControllerDelegate, UINavigationControllerDelegate {
var parent: ImagesPicker
init(parent: ImagesPicker) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
if !results.isEmpty {
parent.itemProvider = []
parent.selectedImages = []
}
parent.itemProvider = results.map(\.itemProvider)
loadImage()
}
private func loadImage() {
for itemProvider in parent.itemProvider {
if itemProvider.canLoadObject(ofClass: UIImage.self) {
itemProvider.loadObject(ofClass: UIImage.self) { image, error in
DispatchQueue.main.sync {
if let image = image as? UIImage {
self.parent.selectedImages?.append(image)
}
}
}
}
}
}
}
}
struct PickerHomeView: View {
#EnvironmentObject var vm: ViewModel
#State private var isSelected = false
var body: some View {
NavigationView {
VStack {
NavigationLink("Tab View", isActive: $isSelected) {
PickerTabView()
}
.hidden()
HStack {
Button {
vm.showPicker()
} label: {
ButtonLabel(symbolName: "photo.fill", label: "Photos")
}
}
Spacer()
}
.sheet(isPresented: $vm.showPicker) {
ImagesPicker(selectedImages: $vm.images)
.ignoresSafeArea()
}
.onChange(of: vm.images, perform: { _ in
isSelected = true
})
}
}
}
struct PickerSettingView: View {
#EnvironmentObject var vm: ViewModel
var image: UIImage
let myImage = MyImage(category: Category.unCategorized)
#State private var selectedCategory: Category = Category.unCategorized
var body: some View {
VStack {
Image(uiImage: image)
.resizable()
.scaledToFit()
.frame(minWidth: 0, maxWidth:.infinity)
SwiftUI.Picker("Category Picker", selection: $selectedCategory) {
Text("Formal").tag(Category.formal)
Text("Casual").tag(Category.casual)
Text("Semi Formal").tag(Category.semiFormal)
}
.pickerStyle(.segmented)
.padding([.leading, .trailing], 16)
HStack {
Button {
if vm.selectedImage == nil {
vm.addMyImage(category: selectedCategory, image: image)
} else {
vm.updateSelected()
}
} label: {
ButtonLabel(symbolName: vm.selectedImage == nil ? "square.and.arrow.down.fill" :
"square.and.arrow.up.fill",
label: vm.selectedImage == nil ? "Save" : "Update")
}
}
}
}
}
class ViewModel: ObservableObject {
#Published var images: [UIImage]?
#Published var showPicker = false
}
I think the image in your loadImage, is not being added to the self.parent.selectedImages
when it is nil, that is, when images in your ViewModel is nil, as it is at the start. So whenever you try to access any index of the images array in your vm.images, the app crashes.
You could try this in your loadImage (note also .async) to append the images:
DispatchQueue.main.async {
if let image = image as? UIImage {
if self.parent.selectedImages == nil { self.parent.selectedImages = [] }
self.parent.selectedImages!.append(image)
}
}

Swiftui photoOutput for Images not called every time

This is actually the same issue as this post AVCapturePhotoCaptureDelegate photoOutput() not called every time however no one responded to that . I find that on takePic the photoOutput function is called sometimes and not others it is literally 50/50 . I am using Swiftui 2.0 . Does anyone know a work around this or why this issue is happening ? The code to replicate this is actually quite small .It is the code below and then setting permissions in the info.plist for Privacy - Camera usage description and privacy - photo library usage description . I have tried different things and it is literally still a 50/50 on whether photoOutput gets called . When it is not called you will see this in log print("Nil on SavePic:picData") Any suggestions would be great .
import SwiftUI
import AVFoundation
struct CreateStoryView: View {
#StateObject var camera = CameraModel()
#Environment(\.presentationMode) var presentationMode
var body: some View {
ZStack {
// Going to be Camera Preview
CameraPreview(camera: camera)
.ignoresSafeArea(.all, edges: .all)
VStack {
HStack {
Spacer()
Image(systemName: "arrowshape.turn.up.backward.circle.fill")
.foregroundColor(.black)
.padding(.trailing,20)
.background(Color.white)
.clipShape(/*#START_MENU_TOKEN#*/Circle()/*#END_MENU_TOKEN#*/)
.onTapGesture {
if camera.session.isRunning == true {
camera.session.stopRunning()
}
self.presentationMode.wrappedValue.dismiss()
}
if camera.isTaken {
Button(action: camera.reTake, label: { // camera.reTake
Image(systemName: "arrow.triangle.2.circlepath.camera")
.foregroundColor(.black)
.padding()
.background(Color.white)
.clipShape(/*#START_MENU_TOKEN#*/Circle()/*#END_MENU_TOKEN#*/)
})
.padding(.trailing,10)
}
}
Spacer()
HStack{
// If Taken then showing save and again take button
if camera.isTaken{
Button(action: {if !camera.isSaved{camera.savePic()}}, label: {
Text(camera.isSaved ? "Saved" : "Save")
.foregroundColor(.black)
.fontWeight(.semibold)
.padding(.vertical,10)
.padding(.horizontal,20)
.background(Color.white)
.clipShape(Capsule())
})
.padding(.leading)
Spacer()
} else {
Button(action: camera.takePic , label: {
ZStack{
Circle()
.fill(Color.white)
.frame(width: 65, height: 65)
Circle()
.stroke(Color.white,lineWidth: 2)
.frame(width: 75, height: 75)
}
})
}
}.frame(height: 75)
}
}.onAppear(perform: {
camera.Check()
})
}
}
// Camera Model
class CameraModel: NSObject,ObservableObject,AVCapturePhotoCaptureDelegate {
#Published var isTaken = false
#Published var session = AVCaptureSession()
#Published var alert = false
#Published var output = AVCapturePhotoOutput()
// preview ...
#Published var preview: AVCaptureVideoPreviewLayer!
#Published var isSaved = false
#Published var picData = Data(count: 0)
func Check() {
// first checking camera has permission
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
setUp()
return
case .notDetermined:
//retrusting for permission
AVCaptureDevice.requestAccess(for: .video) {(status) in
if status{
self.setUp()
}
}
case .denied:
self.alert.toggle()
return
default:
return
}
}
func setUp() {
// setting up camera
do{
// setting configs...
self.session.beginConfiguration()
// change for your own
let device = AVCaptureDevice.default(.builtInDualCamera,for: .video,position: .back)
let input = try AVCaptureDeviceInput(device: device!)
// checking and adding session
if self.session.canAddInput(input) {
self.session.addInput(input)
}
// same for output
if (self.session.canAddOutput(self.output)) {
self.session.addOutput(self.output)
}
self.session.commitConfiguration()
} catch {
print(error.localizedDescription)
}
}
// take and retake
func takePic(){
DispatchQueue.global(qos: .background).async {
self.output.capturePhoto(with: AVCapturePhotoSettings(), delegate: self)
self.session.stopRunning()
DispatchQueue.main.async {
withAnimation{ self.isTaken.toggle() }
}
}
}
func reTake() {
DispatchQueue.global(qos: .background).async {
self.session.startRunning()
DispatchQueue.main.async {
// withAnimation{ self.isTaken.toggle() }
// clearing
//self.isSaved = false
self.isTaken = false
}
}
}
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
print("photoOutput check")
if error != nil {
return
}
guard var imageData = photo.fileDataRepresentation() else {return}
self.picData = imageData
if isSaved == true {
if !imageData.isEmpty {
imageData.removeAll()
isSaved = false
}
} else {
isSaved = true
}
}
func savePic() {
if UIImage(data: self.picData) == nil {
print("Nil on SavePic:picData")
return
}
let image = UIImage(data: self.picData)!
// saving image
UIImageWriteToSavedPhotosAlbum(image, self, #selector(saveError), nil)
self.isSaved = true
print("saved sucessfully")
}
#objc func saveError(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
print("Save finished!")
}
}
// setting up view for preview
struct CameraPreview: UIViewRepresentable {
#ObservedObject var camera: CameraModel
func makeUIView(context: Context) -> UIView {
let view = UIView(frame: UIScreen.main.bounds)
camera.preview = AVCaptureVideoPreviewLayer(session: camera.session)
camera.preview.frame = view.frame
camera.preview.videoGravity = .resizeAspectFill
view.layer.addSublayer(camera.preview)
camera.session.startRunning()
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}
struct CreateStoryView_Previews: PreviewProvider {
static var previews: some View {
CreateStoryView()
}
}

Presenting two sheets in SwiftUI

I'm trying to present two sheets in SwiftUI. The first sheet (SecondScreen) opens up on the Main Page (tapping the Navigation Tool Bar Icon) and the second sheet is a ShareSheet which should pop up inside the SecondScreen as an option. I have used a Form to build the SecondScreen. In the Simulator and on my device, the ShareSheet doesn't appear. I hope this is just a bug and not something Apple doesn't allow without big UI changes.
I tried to open the ShareSheet, while having the SecondScreen as a .fullScreenCover., instead of .sheet but the button still doesn't react.
Example
import SwiftUI
struct ContentView: View {
#State var showMore: Bool = false
var body: some View {
NavigationView {
Text("Main Page")
.padding()
.navigationBarTitle("Main Page")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
showMore.toggle()
}, label: {
Image(systemName: "ellipsis.circle")
})
.sheet(isPresented: $showMore, content: {
SecondScreen()
})
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct SecondScreen: View {
var body: some View {
NavigationView {
Form {
Section {
Button(action: {
ShareID (Info: "https://www.google.com")}, label: { Text("Share")
})
}
}
}
}
}
}
func ShareID(Info: String){
let infoU = Info
let av = UIActivityViewController(activityItems: [infoU], applicationActivities: nil)
UIApplication.shared.windows.first?
.rootViewController?.present(av, animated: true,
completion: nil)
}
Thank you!
this is another approach to popup your sheets, even works on my mac:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State var showMore: Bool = false
var body: some View {
NavigationView {
Text("Main Page")
.padding()
.navigationBarTitle("Main Page")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { showMore.toggle() }) {
Image(systemName: "ellipsis.circle")
}
.sheet(isPresented: $showMore) {
SecondScreen()
}
}
}
}
}
}
struct SecondScreen: View {
#State var shareIt = false
#State var info = "https://www.google.com"
var body: some View {
Button(action: {shareIt = true}) {
Text("Share")
}
.sheet(isPresented: $shareIt, onDismiss: {shareIt = false}) {
ShareSheet(activityItems: [info as Any])
}
}
}
struct ShareSheet: UIViewControllerRepresentable {
typealias Callback = (_ activityType: UIActivity.ActivityType?, _ completed: Bool, _ returnedItems: [Any]?, _ error: Error?) -> Void
let activityItems: [Any]
let applicationActivities: [UIActivity]? = nil
let excludedActivityTypes: [UIActivity.ActivityType]? = nil
let callback: Callback? = nil
func makeUIViewController(context: Context) -> UIActivityViewController {
let controller = UIActivityViewController(
activityItems: activityItems,
applicationActivities: applicationActivities)
controller.excludedActivityTypes = excludedActivityTypes
controller.completionWithItemsHandler = callback
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) { }
}

How to search a custom text from textfield in webkit swiftUI

I am building a small browser in swift ui i have build a struct to represent the wkwebview and i want to enter any text in a textfield and search on the internet using wkwebview and a google query i tried to reinstantiate the Webview(web: nil, req: URLRequest(url: URL(string: searchText)!)) but its not working for me? how can i achieve this i am pretty new to SwiftUI.Please help?
struct Webview : UIViewRepresentable {
let request: URLRequest
var webview: WKWebView?
init(web: WKWebView?, req: URLRequest) {
self.webview = WKWebView()
self.request = req
}
func makeUIView(context: Context) -> WKWebView {
return webview!
}
func updateUIView(_ uiView: WKWebView, context: Context) {
uiView.load(request)
}
func goBack(){
webview?.goBack()
}
func goForward(){
webview?.goForward()
}
func refresh(){
webview?.reload()
}
}
import SwiftUI
struct ContentView: View {
var webview = Webview(web: nil, req: URLRequest(url: URL(string: "https://www.apple.com")!))
#State private var searchText = ""
#State private var txt = ""
var body: some View {
ZStack {
HStack {
TextField("Search", text: $searchText,onCommit: {
print(searchText)
}
)
.keyboardType(.URL)
.frame(width: UIScreen.main.bounds.size.width * 0.75 )
}
}
webview
.toolbar {
ToolbarItemGroup(placement: .bottomBar) {
Button(action: {
webview.goBack()
}) {
Image(systemName: "arrow.left")
}
Spacer()
Button(action: {
webview.goForward()
}) {
Image(systemName: "arrow.right")
}
Spacer()
Button(action: {
webview.refresh()
}) {
Image(systemName: "arrow.clockwise")
}
}
}
}
}
The way that you have things set up right now (attempting to store a reference to a UIViewRepresentable) is going to lead to problems later on (like this, in fact).
I suggest you set up an interim object of some sort that can be used to store data and communicate imperatively to the WKWebView while still retaining the ability to lay things out declaratively. In my example, WebVewManager is that interim object that both the parent view and the UIViewRepresentable have access to:
class WebViewManager : ObservableObject {
var webview: WKWebView = WKWebView()
init() {
webview.load(URLRequest(url: URL(string: "https://apple.com")!))
}
func searchFor(searchText: String) {
if let searchTextNormalized = searchText.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
let url = URL(string: "https://google.com/search?q=\(searchTextNormalized)") { self.loadRequest(request: URLRequest(url: url))
}
}
func loadRequest(request: URLRequest) {
webview.load(request)
}
func goBack(){
webview.goBack()
}
func goForward(){
webview.goForward()
}
func refresh(){
webview.reload()
}
}
struct Webview : UIViewRepresentable {
var manager : WebViewManager
init(manager: WebViewManager) {
self.manager = manager
}
func makeUIView(context: Context) -> WKWebView {
return manager.webview
}
func updateUIView(_ uiView: WKWebView, context: Context) {
}
}
struct ContentView: View {
#StateObject private var manager = WebViewManager()
#State private var searchText = ""
#State private var txt = ""
var body: some View {
ZStack {
HStack {
TextField("Search", text: $searchText,onCommit: {
print(searchText)
manager.searchFor(searchText: searchText)
})
.keyboardType(.URL)
.frame(width: UIScreen.main.bounds.size.width * 0.75 )
}
}
Webview(manager: manager)
.toolbar {
ToolbarItemGroup(placement: .bottomBar) {
Button(action: {
manager.goBack()
}) {
Image(systemName: "arrow.left")
}
Spacer()
Button(action: {
manager.goForward()
}) {
Image(systemName: "arrow.right")
}
Spacer()
Button(action: {
manager.refresh()
}) {
Image(systemName: "arrow.clockwise")
}
}
}
}
}
I'm doing some simple percent encoding on the search string -- you may need to do more testing to make sure that google always accepts the search query, but this looks like it's working to me.

LazyVGrid, List, LazyStacks don't release views from memory?

I'm playing around with the new photo picker in SwiftUI 2 and I made a simple app to show the imported images in a LazyVGrid but when scrolling down, if I imported around 150 images the app finish all the memory and it crashes (Terminated due to memory issue).
I tried the same with a LazyVStack and List but they have the same problem, I was expecting lazy items to release all the cells that goes off screen from the memory but it doesn't look like it's working.
Is this a bug or am I doing something wrong?
Here's my code:
import SwiftUI
struct Media: Identifiable {
var id = UUID()
var image: Image
}
struct ContentView: View {
#State var itemProviders: [NSItemProvider] = []
#State private var showingPhotoPicker: Bool = false
let columns = [
GridItem(.adaptive(minimum: 100, maximum: 100), spacing: 8)
]
#State var medias: [Media] = []
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: columns, spacing: 8) {
ForEach(medias) { media in
media.image
.resizable()
.scaledToFill()
.frame(width: 100, height: 100, alignment: .center)
.clipped()
}
}
}
.navigationBarTitle("Images \(medias.count)")
.navigationBarItems(leading: Button(action: {
loadImages()
}, label: {
Text("Import \(itemProviders.count) images")
}), trailing: Button(action: {
showingPhotoPicker.toggle()
}, label: {
Image(systemName: "photo.on.rectangle.angled")
}))
.sheet(isPresented: $showingPhotoPicker) {
MultiPHPickerView(itemProviders: $itemProviders)
}
}
}
func loadImages() {
for item in itemProviders {
if item.canLoadObject(ofClass: UIImage.self) {
item.loadObject(ofClass: UIImage.self) { image, error in
DispatchQueue.main.async {
guard let image = image as? UIImage else {
return
}
medias.append(Media(image: Image(uiImage: image)))
}
}
}
}
}
}
And the PhotoPickerView:
import SwiftUI
import PhotosUI
struct MultiPHPickerView: UIViewControllerRepresentable {
#Environment(\.presentationMode) private var presentationMode
#Binding var itemProviders: [NSItemProvider]
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> PHPickerViewController {
var configuration = PHPickerConfiguration()
configuration.filter = .images
configuration.selectionLimit = 0
let controller = PHPickerViewController(configuration: configuration)
controller.delegate = context.coordinator
return controller
}
func updateUIViewController( _ uiViewController: PHPickerViewController, context: Context) {}
class Coordinator: NSObject, PHPickerViewControllerDelegate {
#Environment(\.presentationMode) private var presentationMode
var parent: MultiPHPickerView
init( _ parent: MultiPHPickerView ) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss( animated: true )
self.parent.itemProviders = results.map(\.itemProvider)
}
}
}