Post not yet marked as solved
Since the header of a List can't be customised as much as I'd like it to be, I've used a workaround with a ScrollView and a LazyVStack.
ScrollView {
LazyVStack(pinnedViews: .sectionHeaders) {
ForEach(...)
}
}
However, I can't seem to get the separators right or the amount of padding as well the the default separator insets.
I am making the separators with Divider() but even with dynamic type the views becomes 'broken'.
Is there a way to recreate a List with the standard appearance without using one?
Help is much appreciated.
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()
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
}
Post not yet marked as solved
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)
}
}
}
Post not yet marked as solved
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()
}
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.
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.
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?
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)
}
}
}
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.
Post not yet marked as solved
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 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)
}
}
}
}