Is there any way to add destructive actions (such as a delete button in red) or separators to the new Menu in iOS and iPadOS as shown in the Build/Design with iOS pickers, menus and actions talks?
There's no way that I know of with .contextMenu and I was wondering if there was a way or a workaround.
Post
Replies
Boosts
Views
Activity
Here is my code simplified.
struct TileView: View {
@Binding var editMode: EditMode
var columns = [GridItem(.adaptive(minimum: UIScreen.main.bounds.width / 4))]
var body: some View {
LazyVGrid(columns: columns, spacing: 16) {
ForEach(0 ..< 5, id: \.self) { num in
ZStack(alignment: .topLeading) {
GroupBox(label: Text("Label")) {
Text("Content")
}
.contextMenu {
Button {
} label: {
Label("Delete", systemImage: "trash")
}
}
// this is the work around for the delete action
if editMode == .active {
Button {
} label: {
Label("Delete", systemImage: "minus.circle.fill")
.imageScale(.large)
.foregroundColor(Color(.systemRed))
.backgroundColor(Color(.systemFill)
.clipShape(Circle())
.labelStyle(IconOnlyLabelStyle())
.offset(x: -10, y: -10)
}
}
}
.padding(.horizontal, 4)
}
}
}
.padding(.horizontal)
}
}
Is there any way I can add an .onMove or .onDelete modifier to the ForEach and have those actions work when editing, or something else similar (maybe implementing a List to make it work)?
If not, is there a workaround for the move action (since I already have a delete action workaround)?
Thanks in advance for any solutions.
I have a TypingController which when certain key are pressed will send out notifications. A SwiftUI view will receive those notifications and do something with them.
I am trying to figure out how to add this view controller to SceneDelegate.swift to maybe set this as the window.rootViewController but I am unable to work out how.
Any solutions to this would be appreciated.
Here's my code:
extension Notification.Name {
static let enter = Notification.Name("enter")
static let remove = Notification.Name("remove")
static let submit = Notification.Name("submit")
}
class TypingController: UIViewController {
override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
guard let key = presses.first?.key else { return }
switch key.keyCode {
case .keyboardDeleteOrBackspace:
NotificationCenter.default.post(name: .remove, object: nil)
case .keyboardReturn:
NotificationCenter.default.post(name: .submit, object: nil)
default:
guard let characters = key.characters else { return }
if let number = Int(characters) {
NotificationCenter.default.post(name: .enter, object: number)
} else {
super.pressesBegan(presses, with: event)
}
}
}
}
I have a simple SwiftUI view with a Picker:
struct ContentView: View {
@State private var selection = 0
@State private var showingGame = false
var body: some View {
VStack {
Picker("Choose option", selection: $selection) {
ForEach(0 ..< 10, id: \.self) {
Text("Option \($0)")
}
}
Button("Play") { showingGame = true }
.fullScreenCover(isPresented: $showingGame) {
GeometryReader { geometry in
SpriteView(scene: scene(size: geometry.size, preferredFramesPerSecond: 120)
.frame(width: geometry.size.width, height: geometry.size.height)
}
.ignoresSafeArea()
}
}
}
func scene(size: CGSize) -> SKScene {
let scene = GameScene(size: size, selection: selection)
scene.size = size
scene.anchorPoint = CGPoint(x: 0.5, y: 0.5)
scene.scaleMode = .aspectFill
return scene
}
}
and a SKScene with an custom init():
class GameScene: SKScene {
var size: CGSize
var selection: Int
let player: SKSpriteNode
...
init(size: CGSize, selection: Int) {
self.size = size
self.selection = selection
player = SKSpriteNode(imageNamed: "player\(selection)")
super.init(size: size)
}
...
}
I would like to know how to pass in the current value of selection, after the picker has changed it, to the GameScene instead of the initially set value being passed in all the time.
I have added two methods to an extension of URL so I can load and save and images.
extension URL {
func loadImage(_ image: inout UIImage) {
if let loaded = UIImage(contentsOfFile: self.path) {
image = loaded
}
}
func saveImage(_ image: UIImage) {
if let data = image.jpegData(compressionQuality: 1.0) {
try? data.write(to: self)
}
}
}
I use this extension in a view:
@State private var image = UIImage(systemName: "xmark")!
private var url: URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0].appendingPathComponent("image.jpg")
}
var body: some View {
Image(uiImage: image)
.onAppear {
url.load(&image)
}
.onTapGesture {
url.save(image)
}
}
However this isn't working.
The image isn't loaded from the document directory because its probably isn't being saved.
Is there anyway to rewrite this extension or another alternative to loading and saving a UIImage?
Or is this just a bug with Xcode 12/iOS 14?
I have been experiencing a problem that I can't seem to figure out how it's happening or how to fix it.
When then view below appears, the app crashes and shows this message:
Thread 1: EXC_BREAKPOINT (code=1, subcode=0x1a2120fac)
If I remove the line that sets color to nil the view appears without any crashes.
...
@Binding var color: Color?
var body: some View {
NavigationView { ... }
.onAppear {
switch background {
case .color:
color = nil // error not occurring when this line removed
...
}
}
}
I have looked up the error and worked out its about nil and optionals, but I don't know why setting the variable to nil doesn't work.
Has anyone got any ideas as to why this is happening and how to fix it?
Thanks is advance.
I have a data model with UIImage, Color and TextAlignment in.
I think I have managed to conform Color and TextAlignment to Codable.
I am unsure of how to conform UIImage to Codable.
I have an attempt below which crashes when the app loads, probably because there is something wrong with line 20 because backgroundImage is initially nil.
struct Model: Codable {
private enum CodingKeys: CodingKey {
case image
}
var backgroundColor: Color
var textColor: Color
var textAlignment: TextAlignment
var image: UIImage?
init(backgroundColor: Color = Color(.systemFill), textColor: Color = Color(.label), textAlignment: TextAlignment = .leading, image: UIImage? = nil) {
self.backgroundColor = backgroundColor
self.textColor = textColor
self.textAlignment = textAlignment
self.image = image
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let data = try container.decode(Data.self, forKey: .image)
try self.init(from: decoder)
self.image = UIImage(data: data)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if let image = self.image {
let data = image.jpegData(compressionQuality: 1.0)
try container.encode(data, forKey: .image)
}
}
}
Can anyone correct or point me in the right direction as how to fix this?
If is anything wrong with the Codable conformations below, please let me know.
Any help would be really appreciated.
Color and TextAlignment Codable below:
extension Color: Codable {
private enum CodingKeys: CodingKey {
case color
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self = try container.decode(Color.self, forKey: .color)
try self.init(from: decoder)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self, forKey: .color)
}
}
extension TextAlignment: Codable {
private enum CodingKeys: CodingKey {
case center
case leading
case trailing
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let decoded = try? container.decode(String.self, forKey: .center), decoded == "center" {
self = .center
}
if let decoded = try? container.decode(String.self, forKey: .leading), decoded == "leading" {
self = .leading
}
if let decoded = try? container.decode(String.self, forKey: .trailing), decoded == "trailing" {
self = .trailing
}
try self.init(from: decoder)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .center:
try container.encode("center", forKey: .center)
case .leading:
try container.encode("leading", forKey: .leading)
case .trailing:
try container.encode("trailing", forKey: .trailing)
}
}
}
The error I am getting is: Result values in '? :' expression have mismatching types 'some View' (result of 'Self.redacted(reason:)') and 'some View' (result of 'Self.unredacted()')
But aren't some View and some View the same thing.
This method is part of a view:
func redacted(when active: Bool) -> some View {
active ? redacted(reason: .placeholder) : unredacted()
}
and then used on the view in ContentView like this:
TestView()
.redacted(when: isActive)
Even if I use an if statement this error still occurs: Function declares an opaque return type, but the return statements in its body do not have matching underlying types
How can I return either of two modifiers that both return some View from a method that returns some View?
Seeing as contextMenu as been deprecated and they are Menus shown on a long press, how can that be achieved with SwiftUI?
I haven't found a new initialiser for Menu yet to show on a long press or an isPresented argument.
When the Xcode Previews app opens on the device, this is the error message in Xcode: RemoteHumanReadableError: Failed to update preview.
Error encountered when sending 'display' message to agent.
I tried cleaning the build folder and quitting then reopening Xcode to no avail.
Using Xcode 12.0.1 previewing on iPhone 11.
Is there any way to change this without using a toolbar item, and instead changing it something like this:
.onAppear {
UINavigationBar().backItem?.title = "Title"
// also tried this
UINavigationController().navigationBar.backItem?.title = "Title"
}
which I can’t seem to get working yet.
I have made a font picker but the problem is that I can’t seem to figure out why there is no navigation bar with a cancel button and search bar. Normally there would be but with this there isn’t (there is a fontPickerViewControllerDidCancel method but no cancel button).
Has anyone else had this problem, and is there a way to resolve this?
struct FontPicker: UIViewControllerRepresentable {
class Coordinator: NSObject, UIFontPickerViewControllerDelegate {
private let parent: FontPicker
init(_ parent: FontPicker) {
self.parent = parent
}
func fontPickerViewControllerDidPickFont(_ viewController: UIFontPickerViewController) {
parent.presentationMode.wrappedValue.dismiss()
guard let descriptor = viewController.selectedFontDescriptor else { return }
let font = UIFont(descriptor: descriptor, size: 17)
parent.fontName = font.fontName
}
}
@Environment(\.presentationMode) private var presentationMode
@Binding var fontName: String
func makeUIViewController(context: Context) -> some UIViewController {
let configuration = UIFontPickerViewController.Configuration()
configuration.includeFaces = true
let picker = UIFontPickerViewController(configuration: configuration)
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
}
// used in a View like this
Button("Choose font") { showingFontPicker = true }
.sheet(isPresented: $showingFontPicker) {
FontPicker(fontName: $newFontName)
.ignoresSafeArea()
}
I have a LazyVGrid of items which when tapped I want to showing details of that item.
When the item is tapped the item should animate to the detail view with matchedGeometryEffect. However this doesn’t work. Even with the withAnimation there is no animation.
How can this code work with matchedGeometryEffect?
Any help will be appreciated.
I have replaced the items in the grid with numbers to make it simpler.
struct ContentView: View {
@Namespace private var animation
@State private var showingDetail = false
@State private var selection = -1
var detail: some View {
VStack {
Spacer()
Image(systemName: "\(selection).square")
.resizable()
.scaledToFit()
.matchedGeometryEffect(id: selection, in: animation)
Spacer()
Text("Number \(selection)")
.font(.headline)
.padding(.bottom)
}
}
var grid: some View {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 100, maximum: 100))]) {
ForEach(0 ..< 20) { num in
Image(systemName: "\(num).square")
.resizable()
.scaledToFit()
.frame(width: 100, height: 100)
.matchedGeometryEffect(id: num, in: animation)
.onTapGesture {
selection = num
withAnimation { showingDetail = true }
}
}
}
}
}
var body: some View {
NavigationView {
Group {
if showingDetail {
detail
.toolbar {
Button {
withAnimation { showingDetail = false }
} label: {
Image(systemName: "xmark.circle.fill")
.imageScale(.large)
.foregroundColor(.secondary)
}
}
} else {
grid
}
}
.navigationTitle(showingDetail ? "" : "Numbers")
.navigationBarTitleDisplayMode(showingDetail ? .inline : .automatic)
}
}
}
I have implemented a custom NavigationView with a search bar in SwiftUI.
The SearchResultsContent doesn’t update properly though. I can’t seem to figure out why.
I’ve done all the UISearchResultsUpdating related things, but that doesn’t fix the problem.
struct SearchBarNavigationView<SearchResultsContent: View, Content: View>: UIViewControllerRepresentable {
class Coordinator: NSObject, UISearchResultsUpdating {
private let parent: SearchBarNavigationView
init(_ parent: SearchBarNavigationView) {
self.parent = parent
}
func updateSearchResults(for searchController: UISearchController) {
guard let searchText = searchController.searchBar.text else { return }
DispatchQueue.main.async { self.parent.text = searchText }
}
}
@Binding private var text: String
private let searchResultsContent: SearchResultsContent
private let content: Content
init(text: Binding<String>, @ViewBuilder searchResultsContent: () -> SearchResultsContent, @ViewBuilder content: () -> Content) {
_text = text
self.searchResultsContent = searchResultsContent()
self.content = content()
}
func makeUIViewController(context: Context) -> UINavigationController {
let rootViewController = UIHostingController(rootView: content)
let navigationController = UINavigationController(rootViewController: rootViewController)
navigationController.navigationBar.prefersLargeTitles = true
let searchResultsController = UIHostingController(rootView: searchResultsContent)
let searchController = UISearchController(searchResultsController: searchResultsController)
searchController.searchResultsUpdater = context.coordinator
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.autocapitalizationType = .none
navigationController.navigationBar.topItem?.searchController = searchController
return navigationController
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {
if let searchResultsController = uiViewController.navigationBar.topItem?.searchController?.searchResultsController as? UIHostingController<SearchResultsContent> {
searchResultsController.rootView = searchResultsContent
searchResultsController.view.setNeedsDisplay()
}
if let rootViewController = uiViewController.topViewController as? UIHostingController<Content> {
rootViewController.rootView = content
rootViewController.view.setNeedsDisplay()
}
uiViewController.navigationBar.topItem?.searchController?.searchBar.text = text
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
}
Implementation in View
SearchBarNavigationView(text: $searchText) {
Text(searchText) // doesn’t change whilst typing
} content: {
List(0 ..< 20) {
Text("Row \($0)")
}
}
.ignoresSafeArea()
.onChange(of: searchText) { value in
print(value) // works correctly
}
How can I show a default view when there's no selection like in a normal NavigationView but with a UINavigationController?
I've tried this
} content: {
NavigationView {
Text("Hello, World!")
.navigationTitle("Title")
Text("No selection")
}
.navigationBarHidden(true)
}
but then the search bar doesn't show.
struct SearchBarNavigationView<SearchResultsContent: View, Content: View>: UIViewControllerRepresentable {
class Coordinator: NSObject, UISearchResultsUpdating {
private let parent: SearchBarNavigationView
init(_ parent: SearchBarNavigationView) {
self.parent = parent
}
func updateSearchResults(for searchController: UISearchController) {
guard let searchText = searchController.searchBar.text else { return }
DispatchQueue.main.async { self.parent.text = searchText.trimmingCharacters(in: .whitespaces) }
}
}
@Binding private var text: String
private let searchResultsContent: SearchResultsContent
private let content: Content
init(text: Binding<String>, @ViewBuilder searchResultsContent: () -> SearchResultsContent, @ViewBuilder content: () -> Content) {
_text = text
self.searchResultsContent = searchResultsContent()
self.content = content()
}
func makeUIViewController(context: Context) -> UINavigationController {
let rootViewController = UIHostingController(rootView: content)
let navigationController = UINavigationController(rootViewController: rootViewController)
navigationController.navigationBar.prefersLargeTitles = true
let searchResultsController = UIHostingController(rootView: searchResultsContent)
let searchController = UISearchController(searchResultsController: searchResultsController)
searchController.searchResultsUpdater = context.coordinator
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.autocapitalizationType = .none
navigationController.navigationBar.topItem?.searchController = searchController
return navigationController
}
func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {
if let searchResultsController = uiViewController.navigationBar.topItem?.searchController?.searchResultsController as? UIHostingController<SearchResultsContent> {
searchResultsController.rootView = searchResultsContent
searchResultsController.view.setNeedsDisplay()
}
if let rootViewController = uiViewController.topViewController as? UIHostingController<Content> {
rootViewController.rootView = content
rootViewController.view.setNeedsDisplay()
}
uiViewController.navigationBar.topItem?.searchController?.searchBar.text = text
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
}
SearchBarNavigationView(text: $searchText) {
Text(searchText)
} content: {
Text("Hello, World!")
.navigationTitle("Title")
Text("No selection") // make this show when no selection
}
.ignoresSafeArea()