I have a Home screen that, when a button is pressed, opens a model screen with a form for sending data to the database, inside this model window, as I said, there is a form through NavigationView {, Form { …etc
Inside this form, there is another model window that allows you to select multiple images to upload.
The problem is that because of all this salad, something broke and I just can’t understand what exactly, maybe someone from the outside will see what the problem is.
I see an error [Missing arguments for parameters ‘onEnd’, ‘onSelect’ in call] here:
struct AddItemView_Previews: PreviewProvider {
static var previews: some View {
AddItemView()
}
}
When I change it to PhotosView it causes other errors – (also related to needing to change AddItemView to PhotosView) on the Home screen (which has a button that opens AddItemView which is the problem)
Big Thanks!
All Stuff:
AddItemView
enum Mode {
case new
case edit
}
enum Action {
case delete
case done
case cancel
}
struct AddItemView: View {
@Environment(\.presentationMode) private var presentationMode
@State var presentActionSheet = false
// MARK: - State (Initialiser-modifiable)
@ObservedObject var viewModel = NewItemView()
var mode: Mode = .new
var completionHandler: ((Result<Action, Error>) -> Void)?
// MARK: - UI Components
var cancelButton: some View {
Button(action: { self.handleCancelTapped() }) {
Text("Cancel")
}
}
var saveButton: some View {
Button(action: { self.handleDoneTapped() }) {
Text(mode == .new ? "Done" : "Save")
}
.disabled(!viewModel.modified)
}
// MARK: Connecting image-picker View Model
@StateObject var imagePickerModel: ImagePickerViewModel = .init()
@Environment(\.self) var env
// MARK: Callbacks
var onEnd: ()->()
var onSelect: ([PHAsset])->()
var body: some View {
NavigationView {
Form {
Section(header: Text("New Item")) {
TextField("Title", text: $viewModel.singleitem.title)
TextField("Description", text: $viewModel.singleitem.description)
}
Section(header: Text("Author")) {
TextField("Author", text: $viewModel.singleitem.author)
}
if mode == .edit {
Section {
Button("Delete item") { self.presentActionSheet.toggle() }
.foregroundColor(.red)
}
}
let deviceSize = UIScreen.main.bounds.size
VStack(spacing: 0) {
HStack {
Text("Select Image")
.font(.callout.bold())
.frame(maxWidth: .infinity, alignment: .leading)
Button {
onEnd()
} label: {
Image(systemName: "xmark.circle.fill")
.font(.title3)
.foregroundColor(.primary)
}
}
.padding([.horizontal, .top])
.padding(.bottom, 10)
ScrollView(.vertical, showsIndicators: false) {
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 10), count: 4),
spacing: 12) {
ForEach($imagePickerModel.fetchedImages){$imageAsset
in
// MARK: Grid Content
GridContent(imageAsset: imageAsset)
.onAppear {
if imageAsset.thumbnail == nil {
// MARK: Fetching thumbnail image
let manager = PHCachingImageManager.default()
manager.requestImage(for: imageAsset.asset, targetSize: CGSize(width: 100, height: 100), contentMode: .aspectFill, options: nil) { image,
_ in
imageAsset.thumbnail = image
}
}
}
}
}
.padding()
}
.safeAreaInset(edge: .bottom) {
// MARK: Add button
Button {
let imageAssets = imagePickerModel.selectedImages.compactMap {
ImageAsset -> PHAsset? in
return ImageAsset.asset
}
onSelect(imageAssets)
} label: {
Text("Add\(imagePickerModel.selectedImages.isEmpty ? "" : "\(imagePickerModel.selectedImages.count)Images")")
.font(.callout)
.fontWeight(.semibold)
.foregroundColor(.white)
.padding(.horizontal, 30)
.padding(.vertical, 10)
.background {
Capsule()
.fill(.blue)
}
}
.disabled(imagePickerModel.selectedImages.isEmpty)
.opacity(imagePickerModel.selectedImages.isEmpty ? 0.6 : 1)
.padding(.vertical)
}
}
.frame(height: deviceSize.height / 1.8)
.frame(maxWidth: (deviceSize.width - 40) > 350 ? 350 : (deviceSize.width - 40))
.background {
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(env.colorScheme == .dark ? .black : .white)
}
// MARK: Since its an overlay view
// Making it to take full screen space
.frame(width: deviceSize.width, height: deviceSize.height, alignment: .center)
}
}
// MARK: Grid Image Content
.navigationTitle(mode == .new ? "Item" : viewModel.singleitem.title)
.navigationBarTitleDisplayMode(mode == .new ? .inline : .large)
.navigationBarItems(
leading: cancelButton,
trailing: saveButton
)
.actionSheet(isPresented: $presentActionSheet) {
ActionSheet(title: Text("Are you sure?"),
buttons: [
.destructive(Text("Delete item"),
action: { self.handleDeleteTapped() }),
.cancel()
])
}
}
// MARK: - Action Handlers
func handleCancelTapped() {
self.dismiss()
}
func handleDoneTapped() {
self.viewModel.handleDoneTapped()
self.dismiss()
}
func handleDeleteTapped() {
viewModel.handleDeleteTapped()
self.dismiss()
self.completionHandler?(.success(.delete))
}
func dismiss() {
self.presentationMode.wrappedValue.dismiss()
}
func GridContent(imageAsset: ImageAsset)-> some View {
GeometryReader { proxy in
let size = proxy.size
ZStack {
if let thumbnail = imageAsset.thumbnail {
Image(uiImage: thumbnail)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: size.width, height: size.height)
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
} else {
ProgressView()
.frame(width: size.width, height: size.height, alignment: .center)
}
ZStack {
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(.black.opacity(0.1))
Circle()
.fill(.white.opacity(0.25))
Circle()
.stroke(.white, lineWidth: 1)
if let index = imagePickerModel.selectedImages.firstIndex(where: { asset
in
asset.id == imageAsset.id
}) {
Circle()
.fill(.blue)
Text("\(imagePickerModel.selectedImages[index].assetIndex + 1)")
.font(.caption2.bold())
.foregroundColor(.white)
}
}
.frame(width: 20, height: 20)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
.padding(5)
}
.clipped()
.onTapGesture {
// MARK: Adding / Removing Asset
withAnimation(.easeInOut) {
if let index = imagePickerModel.selectedImages.firstIndex(where: { asset in
asset.id == imageAsset.id
}) {
// MARK: Remove and Update Selected Index
imagePickerModel.selectedImages.remove(at: index)
imagePickerModel.selectedImages.enumerated().forEach {
item in
imagePickerModel.selectedImages[item.offset].assetIndex =
item.offset
}
} else {
// MARK: Add new
var newAsset = imageAsset
newAsset.assetIndex = imagePickerModel.selectedImages.count
imagePickerModel.selectedImages.append(newAsset)
}
}
}
}
.frame(height: 70)
}
}
struct AddItemView_Previews: PreviewProvider {
static var previews: some View {
AddItemView()
}
}
extension View {
func endTextEditing() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder),
to: nil, from: nil, for: nil)
}
}
ImagePickerViewModel
class ImagePickerViewModel: ObservableObject {
// MARK: Properties
@Published var fetchedImages: [ImageAsset] = []
@Published var selectedImages: [ImageAsset] = []
// MARK: Fetching Images
init() {
fetchImages()
}
func fetchImages() {
let options = PHFetchOptions()
// MARK: Need modify after finishing
options.includeHiddenAssets = false
options.includeAssetSourceTypes = [.typeUserLibrary]
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
PHAsset.fetchAssets(with: .image, options: options).enumerateObjects { asset, _, _ in
let imageAsset: ImageAsset = .init(asset: asset)
self.fetchedImages.append(imageAsset)
}
}
}
PhotosView
struct PhotosView: View {
// MARK: Picker properties
@State var showPicker: Bool = false
@State var pickedImages: [UIImage] = []
var body: some View {
NavigationView {
TabView {
ForEach(pickedImages, id: \.self) { image in
GeometryReader { proxy in
let size = proxy.size
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: size.width, height: size.height)
.cornerRadius(15)
}
.padding()
}
}
.frame(height: 450)
// MARK: Swift UI Bug
.tabViewStyle(.page(indexDisplayMode: pickedImages.isEmpty ? .never : .always))
.toolbar {
Button {
showPicker.toggle()
} label: {
Image(systemName: "plus")
}
}
}
.popupImagePicker(show: $showPicker) { asset in
// MARK: Example of Kavsoft
let manager = PHCachingImageManager.default()
let options = PHImageRequestOptions()
options.isSynchronous = true
DispatchQueue.global(qos: .userInitiated).async {
asset.forEach { asset in
manager.requestImage(for: asset, targetSize: .init(), contentMode: .default, options: options) { image, _ in
guard let image = image else { return }
DispatchQueue.main.async {
self.pickedImages.append(image)
}
}
}
}
}
}
}
struct PhotosView_Previews: PreviewProvider {
static var previews: some View {
PhotosView()
}
}
Extension
extension View {
@ViewBuilder
func popupImagePicker(show: Binding<Bool>, transition: AnyTransition = .move(edge: .bottom), onSelect: @escaping ([PHAsset])->())-> some View {
self
.overlay {
let deviceSize = UIScreen.main.bounds.size
ZStack {
// MARK: Bg blur
Rectangle()
.fill(.ultraThinMaterial)
.ignoresSafeArea()
.opacity(show.wrappedValue ? 1 : 0)
.onTapGesture {
show.wrappedValue = false
}
if show.wrappedValue {
AddItemView {
show.wrappedValue = false
} onSelect: { assets in
onSelect(assets)
show.wrappedValue = false
}
.transition(transition)
}
}
.frame(width: deviceSize.width, height: deviceSize.height)
.animation(.easeInOut, value: show.wrappedValue)
}
}
}
ImageAsset
struct ImageAsset: Identifiable {
var id: String = UUID().uuidString
var asset: PHAsset
var thumbnail: UIImage?
// MARK: Selected Image Index
var assetIndex: Int = -1
}