Post

Replies

Boosts

Views

Activity

How to draw emojis like the Lock Screen customisation?
On iOS you can create a new Lock Screen that contains a bunch of emoji, and they'll get put on the screen in a repeating pattern, like this: When you have two or more emoji they're placed in alternating patterns, like this: How do I write something like that? I need to handle up to three emoji, and I need the canvas as large as the device's screen, and it needs to be saved as an image. Thanks! (I've already written an emojiToImage() extension on String, but it just plonks one massive emoji in the middle of an image, so I'm doing something wrong there.)
0
0
113
1w
Biometric authentication, Face ID doesn't get triggered
When a user swipes up to see the app switcher, I put a blocking view over my app so the data inside cannot be seen if you flick through the app switcher. I do this by checking if the scenePhase goes from .active to .inactive. If the app goes into the background, scenePhase == .background so I trigger something that would force the user to authenticate with Face ID/Touch ID when the app is next brought to the foreground or launched. However, this doesn't seem to work. The biometrics authentication is executed, but it just lets the user in without showing the Face ID animation. I put my finger over the sensors so it couldn't possibly be authenticating, but it just lets them in. Here's a quick set of logs: scenePhase == .inactive - User showed app switcher scenePhase == .background - User swiped up fully, went to Home Screen scenePhase == .inactive - User has tapped the app icon scenePhase == .active - App is now active authenticate() - Method called authenticate(), authenticateViaBiometrics() == true - User is going to be authenticated via Face ID // Face ID did not appear! success = true - Result of calling `context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics` means user was authenticated successfully error = nil - No error in the authentication policy authenticate(), success - Method finished, user was authenticated Here's the code: print("authenticate(), authenticateViaBiometrics() == true - User is going to be authenticated via Face ID") var error: NSError? guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else { // Handle permission denied or error print("authenticate(), no permission, or error") authenticated = false defaultsUpdateAuthenticated(false) defaultsUpdateAuthenticateViaBiometrics(false) return } context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: "Authenticate with biometrics") { (success, error) in DispatchQueue.main.async { print("success = \(success)") print("error = \(String(describing: error?.localizedDescription))") if(success) { print("authenticate(), success") authenticated = true } else { print("authenticate(), failure") authenticated = false } } } This happens with or without the DispatchQueue... call.
2
0
536
Aug ’24
Complying with the EU's Digital Services Act: Trader status
According to this Apple page, if you make any money from your apps in the EU you have to provide your email address, phone number and address, and they will be displayed on your App Store page for all and sundry to see, use, and likely, abuse. I don't want anyone and everyone to know those details; they are private. I thought Apple was all about privacy? I understand they have to adhere to the DSA, but Apple hasn't raised a single objection to this. Apple has consistently said that not sharing a user's email address with a developer is a part of being in the App Store, i.e. Spotify can't contact someone who downloaded their app; but a user can now contact the developer? I barely make any money from my apps - not even enough to cover the annual developer program fee - but I keep developing to stay current. I cannot afford a PO Box or business address and phone number to shield me from this, so I'm likely to remove my apps from the EU market. You might think I'm being overly-cautious, or having a knee-jerk reaction, but these are my personal, private details, and they should not be available publicly just because I barely clear £1.50 a month from my apps.
0
0
376
Aug ’24
How to use the new Text timer formats?
I'm trying to get a countdown timer to work, and the way I currently do it in my watchOS 10 app is a complicated load of nonsense to get two Strings that look like "1w 1d" and "12:34:56", i.e. a String that shows components like year, week and day, and another String showing hours, minutes and seconds. The new Text formats seen here https://developer.apple.com/wwdc24/10144?time=645 look useful, but I can't get them to return the values I need. If I use this: let dateA = Date.now let dateB = Date.advanced(by: /* value for 8 days, 12 hours, 34 minutes and 56 seconds */) Text(dateA, format: .offset(to: dateB, allowedFields: [.year, .week, .day], maxFieldCount: 3)) I expect to see "1 week 1 day", but it always comes back as "8 days". I guess it's giving me the most concise result, but I don't want that. I'm not sure anyone would want that. Imagine you have an event coming up in 3 days 6 hours, do you want to see "in 78 hours" or do you want "in 3 days and 6 hours"? Why must we make the user calculate the days and hours in their head when we have the ability to give them the right information? While I'm on this, why does the resulting String have to have "in " at the beginning? I don't want that; it's not my use case, but it's forced on me. I've raised this a hundred times with Apple. I just want to see a String that shows the difference between two dates in a format of my choosing, i.e. "1w 1d", but they never give me what I need, so I have to write extremely complex - and fragile - code that figures this stuff out, and I still can't get that to work properly. Why can't we just have something like: Text(from: dateA, to: dateB, format: "%yy %ww %dd") // for "1 year 2 weeks 3 days", show parts with a value > 0 Text(from: dateA, to: dateB, format: "%0yy %0ww %0dd") // for "0 years 2 weeks 3 days", show all parts regardless of value Text(from: dateA, to: dateB, format: "%y %w %d") // for "1y 2w 3d", show parts with a value > 0 Text(from: dateA, to: dateB, format: "%0y %0w %0d") // for "0y 2w 3d", show all parts regardless of value
3
0
586
Jul ’24
How do I get my widgets to work?
iOS app with Home Screen and Lock Screen widgets written in Swift/SwiftUI. I've never been able to get widgets to work properly. It's more pronounced on Lock Screen widgets, so let's try that method first... The app stores data in Core Data as an Event. They're read into my model and stored as WidgetEventDetails structs: struct WidgetEventDetails: AppEntity, Identifiable, Hashable { public var eventId: String public var name: String public var date: Date public var id: String { eventId } This all works absolutely fine in the iOS app, and each one is unique based on the eventId. When I go to add a Lock Screen widget, I customise the Lock Screen, tap in the section to add a widget, and my widgets appear correctly and are selectable: (bottom right, says "1y 28w 1d") So, I tap it and it appears in the widgets section: But it appears as "17w 6d", which is a different event entirely. Notice how the one in the selectable widgets has changed to "15w 5d", and the one I tapped (1y 28w 1d) is nowhere to be seen. So, I tap the one in the top row (17w 6d) to select an event, and this appears, suggesting that the event is the "Edinburgh & Glasgow 2024-02" event: But that event is actually only a day away (1d), so that's not the one I selected at all. I tap the list and see these events: I select "Las Vegas 2024", which is 17w 3d away, and this is shown: 17w 6d is a different event, not Las Vegas 2024. So, I tap it again and see this. The "Loading" text appears for ages, but occasionally does show the full list, as before: I select "Edinburgh & Glasgow 2024-02" which is 1d away, and I see this again: So, I resign myself to hoping it'll just figure itself out, and I tap "Done": "17w 6d" again :( I finish customising, and exit the customisation screen. I show the Lock Screen, and I see this: Why doesn't this work? Here's the code: @main struct WidgetExtensionBundle: WidgetBundle { @WidgetBundleBuilder var body: some Widget { WidgetExtension() } } struct WidgetExtension: Widget { var body: some WidgetConfiguration { AppIntentConfiguration(kind: kWidgetKind, intent: WidgetEventIntent.self, provider: WidgetEventTimelineProvider()) { entry in WidgetEntry(entry: entry) .environment(modelData) } .configurationDisplayName(NSLocalizedString("AddingWidget_Title", comment: "Adding the Widget")) .description(NSLocalizedString("AddingWidget_Description", comment: "Adding the Widget")) .supportedFamilies([.accessoryCircular, .accessoryInline, .accessoryRectangular, .systemSmall, .systemMedium]) .contentMarginsDisabled() } } struct WidgetEventIntent: WidgetConfigurationIntent { static let title: LocalizedStringResource = "AddingWidget_Title" static let description = IntentDescription(LocalizedStringResource("AddingWidget_Description")) @Parameter(title: LocalizedStringResource("Event")) var event: WidgetEventDetails? init(event: WidgetEventDetails? = nil) { self.event = event } init() {} static var parameterSummary: some ParameterSummary { Summary { \.$event } } } struct EventQuery: EntityQuery, Sendable { func entities(for identifiers: [WidgetEventDetails.ID]) async throws -> [WidgetEventDetails] { modelData.availableEvents.filter { identifiers.contains($0.id) } // availableEvents is just [WidgetEventDetails] } func suggestedEntities() async throws -> [WidgetEventDetails] { return modelData.availableEvents.filter { $0.type == kEventTypeStandard } } } If you think it's the TimelineProvider causing it, I can provide that code, too.
2
0
1.1k
Mar ’24
How to resize a UIImage and get rid of the white line?
Hi all. I've spent six hours today trying to resize images from the Photos Library on the iPhone Simulator, but no matter what I try - and I've tried everything on StackOverflow - I always get a white line at the bottom of the image, even when trying the suggestions that specifically say "this one even gets rid of the white line!" This is just one of the many attempts: let image: UIImage = <whatever> let size = CGSize(width: 800, height: 600) guard let data = resizeImage(image: image, size: size, scale: 1).jpegData(compressionQuality: 0.75) ?? image.pngData() else { return false } func resizeImage(image: UIImage, size: CGSize, scale: CGFloat) -> UIImage { let availableRect = AVFoundation.AVMakeRect(aspectRatio: image.size, insideRect: .init(origin: .zero, size: maxSize)) let targetSize = availableRect.size let format = UIGraphicsImageRendererFormat() format.scale = scale let renderer = UIGraphicsImageRenderer(size: targetSize, format: format) let resized = renderer.image { (context) in image.draw(in: CGRect(origin: .zero, size: targetSize)) } return resized } I've read that image.draw(in:) adds that white line because it's using non-integer values and the background of a JPEG is white, so I floor()d and round()ed the size values, but I always get the white line. I just can't seem to get it to work. Does anyone have a working function that will resize one of the JPEG images included in the Simulator without putting that white line down there? Note: The pink/purple flowers image always resizes and comes back upside down, and the only difference I can see in the six images included in the Simulator is that that one is HEIF while all the others are JPEG. I'll also need any function to handle HEIF/HEIC, too, I guess... This is on Xcode 15.0.1 (15A507) with Simulator iOS 17.0.1. I've also tried it on a real device (iPhone 15 Pro Max, iOS 17.4) and the same thing happens, so it's not like the Simulator images are banjaxed in some way.
3
0
827
Mar ’24
Lightweight migration in Swift/UI with CoreData
Hey all. I've been rewriting my Objective-C app in SwiftUI (it's going well, thanks!) and I'm hitting this issue that I thought I'd solved ages ago. I have version 5.1 of my original ObjC app using version 11 of the Core Data model. I've created a new version 12, added a couple of new attributes, and created a mapping model from 11 to 12. The fields I've added are booleans, and I've set them to have a default value of NO so that new entries will automatically get NO, but there are existing records that don't have that field at all, and I'd like them to also get NO (or YES, depending on something in the other fields, but I haven't figured out how to do that yet). This is how Core Data is set up in the new Swift target: public extension URL { // Returns a URL for the given app group and database pointing to the sqlite database static func storeURL(for appGroup: String, databaseName: String) -> URL { guard let fileContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroup)?.appending(path: kCoreDataFolder) else { logger.error("Shared file container could not be created.") fatalError("CoreData: Shared file container could not be created.") } return fileContainer.appendingPathComponent("\(kCoreDataModel)") } } // MARK: - Persistence Controller struct CoreData { static let shared = CoreData() let container: NSPersistentContainer init() { let storeURL = URL.storeURL(for: kAppGroup, databaseName: kCoreDataModel) let storeDescription = NSPersistentStoreDescription(url: storeURL) container = NSPersistentContainer(name: kCoreDataModel) container.persistentStoreDescriptions = [storeDescription] container.loadPersistentStores(completionHandler: { (storeDescription, error) in if let error = error as NSError? { logger.error("Failed to initialise Managed Object Model from url: \(storeURL), with error: \(error), \(error.userInfo)") fatalError("CoreData: Failed to initialise Managed Object Model from url: \(storeURL), with error: \(error), \(error.userInfo)") } }) container.viewContext.automaticallyMergesChangesFromParent = true } When I run the old app on the Simulator it adds some demo data (using model v11), and the app functions properly. If I then install the new version - using v12 - it fails with a Core Data error because it couldn't migrate the data. (Apologies, I cannot get the exact error at the moment as I'm part-way through redoing some other stuff and the app won't build.) I've read somewhere that "When we use the NSPersistentContainer class to create and manage the Core Data stack, we don't need to do any additional setup work, the lightweight migration is automatically activated for us." But if that's the case, why does my app crash? I'll try and get the exact error for you... How do I implement lightweight migration in Swift? This page suggests creating a persistent coordinator and adding a couple of options, but I can't quite figure out how to do that with the code I already have, and each time I try it seems to beat the original store so I keep going back to the above code because it works.
1
0
917
Mar ’24
SwiftUI is pressing both buttons in a List
I have a List which acts like a form where you can enter values. It uses a custom View struct SectionArea to separate the various sections and cut down on code duplication, like this: List { SectionArea(title: "Section One", height: 176, horizontalPadding: 0, roundCorners: (0, 0, 0, 0)) { VStack { Button { print("Pressed Button One") fullScreenViewChoice = .optionOne showSubView.toggle() } label: { HStack { Text("Button One") Spacer() Image(systemName: "chevron.right") } } .frame(height: 40) Divider() Button { print("Pressed Button Two") fullScreenViewChoice = .optionTwo showSubView.toggle() } label: { HStack { Text("Button Two") Spacer() Image(systemName: "chevron.right") } } .frame(height: 40) Divider() } } } .listStyle(.plain) .listRowSpacing(0) It works fine, but regardless of which button I press it always acts as though both buttons have been pressed. Say I press Button One, the console will display: Pressed Button One Pressed Button Two Can someone tell me where I'm going wrong here, please? Thanks! EDIT: Actually, it doesn't look like it's because of the SectionArea struct, because I've taken the code from there and wrapped it around the content directly, rather than using that struct, and it still does it. I've removed everything and just put this: List { VStack { ButtonOne ButtonTwo } } It still presses both buttons.
2
1
2.2k
Feb ’24
Convert ObjC+CoreData to Swift/SwiftUI+SwiftData?
I have an iOS app that's written in Objective-C, uses Core Data, and has a number of SwiftUI targets (Widgets, Watch app). I'd like to convert the main app to SwiftUI and keep access to the data in the Core Data stack, but move to SwiftData immediately. Since I'm doing a lot of rewriting, it makes sense to leap ahead rather than have to rewrite it in a year or two. Effort isn't an issue; I'm a tenacious SOB ;) But I have no idea how to do this, and can't find any examples on the net of this particular scenario. All I can find is how to start using SwiftData instead of Core Data in an app that's already written in Swift. So, how do I go about the migration without losing data? I guess I'll need to add a new Swift/SwiftUI target for the main app, but then how do I migrate the Core Data store over? In ObjC there's a lot of messing with stacks and the actual location of the model in the filesystem, but I doubt this is necessary in the new way of doing things? Any help would be appreciated. Thanks!
0
0
542
Feb ’24
How to calculate differences between dates for a widget timer
I have a countdown/up timer in a widget, and I want to format the timer to something more readable than the default that Text.init(myDate, style: .timer) provides. The default outputs a timer for just the hours in the date range. So, for example, a timer of 1 week, 5 hours, 12 minutes and 45 seconds will appear as 173:12:45 (which is 7 * 24 + 5 = 173) - not very user-friendly. An ideal output would be 1 week 05:12:45. Is there any way of doing that? I've tried a number of different ways using TimeInterval, DateInterval, and modding (%) values - like modding the hours count by 168 to get a number of weeks - but they're pretty much useless when there are never a set number of days or weeks in a year due to leap years. It would be great if you would provide actual code examples rather than saying to use a certain API, as I could very easily go down a rabbit hole like I have with TimeInterval and DateInterval. Thanks!
0
0
631
Jan ’24
Timers in widgets, how to calculate correct time
I have a Home Screen widget that contains a timer counting down to a specific date. In this image you can see the date calculations: eventDate: 25th Dec 2023 at 09:00:00 entryDate(Date.now): This is just showing you the date in the first SimpleEntry for the widget. getTimeRemaining(entryDate): This shows the number of seconds from the entryDate to the eventDate, figures out how many days there are ("43 days"), and how many hours:mins:secs to the event time, so "10:48:52". Then there's a second entry, entryDate2, that's one hour later, and the values are appropriately calculated. When I create the timeline entries, I add them for: Now (1 entry) Now plus one hour to a week away (one entry per hour = 167 entries) Event date (1 entry) Event date plus one hour to a week later (one entry per hour = 167 entries) Each SimpleEntry entry contains a dictionary with the relevant timer details, and that's what the widget uses to determine what to display in the timer. SwiftUI lacks any useful formatting for a timer. Even the developer docs state: "Example output: 36:59:01". Who wants to see a timer with 36 hours on it? I want it to say "1 day 12:59:01", so I munge the numbers about and grab the separate parts, converting 36:59:01 into "1 day" and "12:59:01". You can see that in the image above. When the entry date of the timeline is reached and the widget is redrawn, it uses the entry containing the dictionary saying it should display "43 days" and the countdown timer should be 10:48:52, then an hour later the dictionary says it's 43 days 9:48:52, etc. The issue is that the widgets, even though they're supposed to have entries at each hour, will always end up displaying something like "29:17:09". The timeline has an entry at every hour, so it should be using the right values. I've checked, and the right values are in the dictionary, so why does the timer keep getting out of sync? I could cut out a massive amount of my widget code if only Text.init(date, style: .timer) would allow some proper formatting.
2
0
831
Nov ’23
Widgets: Can you have a non-customisable static widget?
I want to add a large widget to my app, but it doesn't make sense to simply expand the information that's displayed in a small or medium widget to fill up the space of a large widget. Let's say I have a bunch of events in my app, and the user can add a small/medium widget to their Home Screen and choose which one of their events the widget shows. This is fine and works well, but for a large widget I want to have a totally different experience. I want to display the next four upcoming events in a list. These events are set by the order they're going to occur (date-wise) so you can't really pick which four you want to display, so I don't want to be able to edit the widget. However, if I add such a widget you can still hold down and edit the widget to select an event. Also - and I'm not sure why it does this - when you hold down and edit the widget, it displays the title and description that are assigned to my small/medium AppIntentConfiguration widget, and not the title and description that are provided to the large StaticConfiguration widget, i.e.: struct WidgetExtension: Widget // Small/medium dynamic widget { var body: some WidgetConfiguration { AppIntentConfiguration( kind: kWidgetKind, intent: WidgetEventIntent.self, provider: WidgetEventTimelineProvider() ) { entry in WidgetEntry(entry: entry) } .configurationDisplayName(smallMediumTitle). // This and the line below... .description(NSLocalizedString(smallMediumDescription) // are seen when editing the large widget .supportedFamilies([.systemSmall, .systemMedium]) } } struct WidgetExtension_Large: Widget // Large static widget { var body: some WidgetConfiguration { StaticConfiguration( kind: kWidgetKind, provider: WidgetEventTimelineProvider_Large() ) { entry in WidgetEntry_Large(entry: entry) } .configurationDisplayName(largeTitle) .description(largeDescription) .supportedFamilies([.systemLarge]) } } If it's not possible to have a non-editable widget, it might be more fun to let the user select four events themselves, so I'd need to change the large widget to an AppIntentConfiguration widget, but what would the four parameters look like? Currently, I have this for the small/medium widget that lets you select one event: @Parameter(title: "Event") var event: WidgetEventDetails? init(event: WidgetEventDetails? = nil) { self.event = event } init() { } static var parameterSummary: some ParameterSummary { Summary { \.$event } } How would I change it to select four events? It might be helpful to only be able to select a second event if a first one has been chosen, and a third if a first & second have been chosen etc. Any ideas? Thanks.
2
0
707
Oct ’23
Complications only allow you to select from 15 items?
Just finalising some work on my app update, and it seems that when you go to select an item to show in a complication, when you select your app in the list, the subsequent list only shows 15 of your items. If a user of my app has transferred 20 items to their Watch, they can't select five of them to be shown in a complication. Is that right? If that's a hard limit then I need to be able to separate them out into bunches of 15 items, or maybe have them display under A-E, F-J etc. Does this have to be done as a separate Widget in the WidgetBundle? And how do I do that? Given that I currently have one widget in that bundle that should show everything (20 items), how would I split it out to show an "A-E" widget with those items beginning with A...E? Do I have to have an A-E widget with its own set of data?
2
0
786
Oct ’23
Pulling my hair out with Widgets
Here's my WidgetConfigurationIntent: WidgetEventDetails is a struct conforming to AppEntity, Identifiable, Hashable. It contains all the data needed to show in a widget. Here's my placeholder, snapshot, timeline etc.: When I go to add a widget to my iPhone Home Screen I choose my app, and the small and medium widgets are displayed, and they use the data from the placeholder function correctly, i.e. a random set of data (an 'event') is returned and seen in the widget picker. I select a widget and it appears on the Home Screen, but it's now just a placeholder view, and it stays like that forever. If I hold down and edit the widget, there's no selected event, as in, I might've picked "Christmas" from the widget picker but when it gets added to the Home Screen that link is lost. So, I edit the widget and go to choose any event, and this appears for ages: When it finally displays the list of events I pick one and the widget looks this forever: I can't see what I'm doing wrong. Any ideas? If you need any more of the code, please just ask. I'm getting really frustrated with this stuff and just want it to work. I get so far with it, it all goes well, then something happens and it just breaks. And it's not my coding as I'm using git and can can go back to previous commits where stuff was working, only to find it doesn't. I'm glad iOS 17 now has this "State of Mind" logging 'cos it shows exactly how I feel developing for iOS! 🥸
2
0
905
Oct ’23
PLEASE - how do you structure an app with Widgets and Complications?
Current project structure: Main iOS app (targets iOS 17). Widget Extension (targets iOS 17). Watch app (targets watchOS 10). Complications Extension (a Widget Extension, targets watchOS 10). I did have the complications embedded within the Watch app, but you cannot have two @mains in the same target so the complications are in their own WidgetKit extension. Sadly, when you add a WidgetKit extension to your project, Xcode ALWAYS sets it as an iOS widget extension; it doesn't give you the choice to make it a watchOS extension so you have to figure it out yourself. Now, I'm hitting an issue where the main iOS app works fine, the iOS widgets work fine, the Watch app runs fine, but the complications dot show up anywhere. I can't preview them in Xcode because it says various things like: This app was not built to support this device family; app is compatible with ( 1,2 ) but this device supports ( 4 } I can't find any information on what goes where in the build settings. Can someone tell me how each bit of the project should look in the build settings, please? (There's no information anywhere in Apple's developer documentation - which is a bit weird. They've released Xcode and they have no information on the various bits. And why doesn't Xcode set up a WidgetKit extension properly when you add it? Must we all do this manually, Apple?)
2
0
1.4k
Oct ’23