How to make UserDefaults property wrapper
Issue #726
1 | @propertyWrapper |
Then we can use it as property and provide default value
1 | final class KeepHistoryService { |
Issue #726
1 | @propertyWrapper |
Then we can use it as property and provide default value
1 | final class KeepHistoryService { |
Issue #725
When you want to check for existence using Bool
, consider using Set
over Dictionary with Bool
, as Set
guarantee uniqueness. If using Dictionary
instead, the value for key is Optional<Bool>
where we have to check for both optional and true false within.
1 | struct Book: Hashable { |
Issue #724
We can use .blur
modifier, but with VisualEffectView
gives us more options for material and blending mode.
1 | public struct VisualEffectView: NSViewRepresentable { |
Issue #723
Use @ViewBuilder
to build dynamic content for our HUD. For blur effect, here I use NSVisualEffectView
, but we can use .blur
modifier also
1 | struct HUD<Content>: View where Content: View { |
Then we can make some wrappers for information and progress HUD
1 | struct HUD<Content>: View where Content: View { |
Updated at 2020-12-26 20:59:10
Issue #722
With Xcode 12, we can fire up Instrument to profile our app. Select SwiftUI template
There are many profiles in that template, I find SwiftUI and Time Profile very useful. Here’s the profile I run for my app PastePal
This shows how many instance of View with body invocation are there, both for SwiftUI views and our app views
Taking a look at SwiftUI profile, it shows that ClipboardCell
is taking most of the time, here over 7 seconds
This shows how much time was spent in each functions and call stack.
Then we drill down to Time Profiler, it shows that NSSharingService.submenu
accounts for 75% of performance issue
With these instruments, I found out that the NSSharingService context menu I added has poor performance.
This below method is used every time Menu
is asked, which causes a significant overload on main thread, resulting in noticeable laggy scrolling
1 | extension NSSharingService { |
The reason is that NSSharingService.sharingServices
requires quite some time to get sharing services. A quick fix is to cache the items using static
variable. In Swift, static variable is like lazy
attribute, it is computed only once on first asked
1 | private static let items = NSSharingService.sharingServices(forItems: [""]) |
There are more to optimize in my case, for example calculation of image and dominant colors. But we should only optimize when seeing real performance issue and after proper instruments
After the fix, the time reduces from over 7 seconds to just less than 200ms
Updated at 2020-12-30 06:37:53
Issue #721
For setFrame
to take effect
1 | mainWindow.setFrame(rect, display: true) |
we can remove auto save frame flag
1 | mainWindow.setFrameAutosaveName("MyApp.MainWindow") |
Issue #720
NSStatusItem
is backed by NSButton
, we can animate this inner button. We need to specify position
and anchorPoint
for button’s layer so it rotates around its center point
1 | guard |
Updated at 2020-12-26 21:21:00
Issue #719
From SwiftUI 2 for macOS 11.0, we have access to Menu
for creating menu and submenu. Usually we use Button
for interactive menu items and Text
for disabled menu items.
The easy way to customize menu with image is to call Menu
with content
and label
. Pay attention to how we use Button
and Label
inside Content
to create interactive menu items
1 | Menu( |
We can also use Image
and Text
separately. By default SwiftUI wraps these inside HStack
automatically for us. For now, color has no effect in Menu
, but it works on Text
1 | Image(systemName: SFSymbol.bookmarkFill.rawValue) |
One way to mitigate this is to use Text with icon font. Here I use my FontAwesomeSwiftUI
There’s a problem that only the first Text
is shown
1 | Text(collection.icon) |
The solution is to concatenate Text
. In SwiftUI, Text
has +
operator that allows us to make cool attributed texts
1 | Text(collection.icon) |
Updated at 2020-12-23 06:35:31
Issue #718
Use NSSharingService.sharingServices(forItems:)
with an array of one empty string gives a list of sharing items. There we show image
and title
of each menu item.
We should cache sharing items as that can cause performance issue
1 | import SwiftUI |
Alternative, you can trigger NSSharingServicePicker
from a button, it shows a context menu with sharing options
Updated at 2020-12-25 22:57:57
Issue #717
Try to use predefined system colors in Human Interface Guidelines for macOS
Here we use this color unemphasizedSelectedTextBackgroundColor
for button background
1 | HStack(spacing: 1) { |
Another thing is List, where we have selected and alternative background colors. We should also use dynamic system colors selectedContentBackgroundColor
and alternatingContentBackgroundColors
1 | VStack { |
Updated at 2020-12-21 05:56:28
Issue #716
I have an enum that conforms to CaseIterable
that I want to show in Picker
1 | enum Position: String, Codable, CaseIterable, Identifiable { |
It compiles and runs just fine, but Picker does not show current selection regardless of any Picker style I choose. It does not update Binding
at all.
The fix is to specify id
, it looks redundant because of enum conforms to Identifiable
, but it fixes the problem
1 | Picker(selection: $preference.position, label: Text("Position")) { |
Reading ForEach once again
1 | init(_ data: Data, content: @escaping (Data.Element) -> Content) |
Available when Data conforms to RandomAccessCollection, ID is Data.Element.ID, Content conforms to View, and Data.Element conforms to Identifiable.
So in our case, we use rawValue
as id
for Identifiable
, so there’s mismatch between our selection
being enum case
and items in ForEach
, which uses rawValue
to uniquely identifies items. So our fix is to explicitly state that we want to use the enum case itself \.self
as id
for ForEach
What we can also do is to declare enum case itself as id
1 | enum Position: String, Codable, CaseIterable, Identifiable { |
selection
in List
and id
used in ForEach
are the sameUpdated at 2020-12-23 06:05:08
Issue #715
I remember this time last year in December 2019, I spent almost every single bit of my free time on Puma because I want a Swift friendly version of fastlane that suits my need and leverages Swift 5 features.
Here’s my review of my work in year 2020.
I started blogging on GitHub issue, starting from Issue 1 Hello world, again, now I have over 670 issues, which were generated into blog posts at my website https://onmyway133.com/
🍏 I used to use onmyway133.github.io domain, but then it feels right to have my own domain
🍎 I used to write a lot at Medium https://medium.com/@onmyway133 for many publications and my own Fantageek publication, I have got 2.3k followers with around 60k views per month
🍓 I list my most favorite articles, usually articles that I spent most time polishing here https://onmyway133.com/writing/
One of my very first articles published on Flawless iOS publication was A better way to update UICollectionView data in Swift with diff framework gets the highest traffic ever, and was rated most trending Swift article for 2018.
My one of few articles published in Medium in 2019 was How to make Auto Layout more convenient in iOS got featured in iOS Dev Weekly, and used to promote my library EasyAnchor
My big article this year is How to test push notifications in simulator and production iOS apps, which was also featured on iOS Dev Weekly, used to summary the changes in push notifications from iOS 7 to iOS 14, and to promote my push notification testing tool Push Hero
My blog at https://onmyway133.com/ has around 15k views each month. I can write proper, lengthy articles to get more views but I don’t want to. I want to write blog about solutions I have found, so that my future self can benefit from it without searching too much.
I have done quite a lot of open source, you can view here Open source. These have helped tons of apps, with 45k+ apps touched, and 3.4m+ downloads as stats on CocoaPods
My 4 libraries this year get inspired by SwiftUI and property wrappers
🍌 Spek leverages property wrapper to provide Spec syntax, similar to Quick, but simpler and can generate tests
🍈 Micro imitates SwiftUI State and ForEach syntax but use UICollectionView with diffable datasource, powered by my another library DeepDiff
🍑 EasySwiftUI contains many extensions and useful modifier that I use in my SwiftUI apps
🥝 FontAwesomeSwiftUI I was tired of using bitmap with dark and light variants, and I can’t use SFSymbols as I want to support macOS 10.15, so FontAwesome is a perfect choice. I couldn’t find library that has support for SwiftUI and easy to use with Swift Package Manager for iOS and macOS, so I made one
Besides, for all my libraries EasyStash, EasyAnchor, EasyTheme, EasyClosure, … I have now support Swift Package Manager, which is nicer to integrate. Thank you CocoaPods for all these years.
There are now 1.6k people following me on GitHub, that means a lot, meaning somehow my work is useful
Lately, I open source awesome-swiftui which I curate all SwiftUI resources, articles and libraries that I find useful for my apps
❤️ One day, I got sponsor from my dear friend Chris for my GitHub open source. Chris is my open source idol who ignited my desire for open source. No one loves open source more than Chris
This means the world to me! If everyone was as lucky as me to just get to inspire one person, the world would be an amazing place. Let's make it amazing together! Keep rocking because you are amazing & you can bring out the best in others! ❤️ https://t.co/pJx9obWH7I
— Christoffer Winterkvist (@zenangst) September 25, 2020
I started making some apps late last year, first I published them on Gumroad, but it didn’t feel right, and then I published all my apps on AppStore. I like sandboxed apps from AppStore because they limit what the apps can do.
Apps without Twitter account and landing page seem off, so in May I started revamping my websites, and I wrote my apps page firstly with pure HTML and CSS, then I rewrote in React, because I like React and Javascript.
Notably, early 2020 I made Push Hero, PastePal then I made a complete overhaul lately with more features, thanks to all the feedback. I also took the time to revamp landing pages a lot, you can check landing pages for Push Hero for example because I have a white label solution
Learn how I did white label landing page using React
I have a lot of ideas, but very little time.
Coding can be done, but design is never finished. When making apps, I feel like I’m not certain at some design decisions and no matter how I landed with some designs, I didn’t feel happy. Then I took some design courses and books.
Below is my tweet that I share some design resources that I found useful
Developer with design skill is like tiger with wings. Here are my recommended courses
— Khoa 🔥 (@onmyway133) October 25, 2020
The UI Design Bootcamp by @designcoursecom a massive UI design course teaches you to build beautiful and well-functioning user interfaces using HTML and CSS.https://t.co/vfusXamuhJ
I used to listen to tech podcasts, then I was bored. Discussions like whether to use MVVM vs MVC, SwiftUI vs Catalyst, Swift vs Objective don’t interest me anymore.
If you ask me for choices, my default answer will be NO. Those who are super passionate about their idea will just ignore my advice and do it anyway.
Then I listen to indie and product development podcasts, it was inspiring. Once I got the mindset, they also became boring.
Now I listen to mostly books on Storytel, some books about habit and making time really make my days.
I came back to Twitter early this year after quitting it for a while because I got enough of all negative and nonsense political debates. But then I found that I can decide who I can follow and what content I want to view. Then I started organizing List, making my own Chrome extensions to automate things and control what I want to view. I have followed quite many indie developers and great product people, I’ve learnt a lot. The downside is I’m overwhelmingly inspired, I can’t sleep.
I try to tweet more about what I’ve learned, sharing articles that I have written. For example here I share about how to gain product idea
How to gain product ideas?
— Khoa 🔥 (@onmyway133) November 22, 2020
1) Scratch your own itch. If you don't have any itch to scratch, stop here. This is awkward. Go travelling. Go exploring the world. The world always has problems and needs solution. pic.twitter.com/07gIoJfJet
Notable this year is the website I make WWDC Together as a place for developers like us to hangout and watch videos together. Each video acts like its own chat room, you can also create private chatroom. My colleagues use this and we watched WWDC together with coke and pizza, it was a lot of fun.
I'm excited to launch @wwdctogether as a free place to watch and hangout during WWDC. We can now view upcoming and previous sessions via modern playback, with a chatroom for each session. Hope this provides a similar WWDC vibe during this hard time ❤️🔥https://t.co/0SgGnUAqst pic.twitter.com/f9H9wDi62P
— Khoa 🔥 (@onmyway133) June 17, 2020
I had the idea like 2 weeks before WWDC, so I had to make it quick. Working with React is fun to me, it is like playing video game.
I was lucky to be asked by John to write a guest post Behind the scenes of WWDC Together with Khoa Pham on his website.
It was also mentioned by Paul in his WWDC wrap up WWDC20: Wrap up and recommended talks
One area that absolutely flourished this year was community organization. Sites like https://wwdcwatch.party and https://wwdctogether.com took a huge amount of work to organize, but meant that people had the chance to have some interaction – had the chance to actually chat about WWDC and share their excitement. I’m really grateful to Michie, Khoa, and other community organizers for making this happen.
I also got to share about in in a well known Norwegian tech website orske WWDC Together lar oss se på Apples konferanse sammen
In 2019, I made several meetup talks, and one pre conf talk for Mobile Era conf
An overview of what we as mobile developers have for #MachineLearning in the session by @onmyway133 at @mobileeraconf Community Evening pic.twitter.com/bgcE3huGkI
— Mobile Meetup Oslo (@mobileoslo) November 6, 2019
In 2020, I’m lucky to be invited to talk in some events
🍅 WWDC Watch Party
Thank you @johnestropia @thesunshinejr @onmyway133 for sharing your experience to the community about Open Source Life and thank you for making our dev life better because of your projects! You are all awesome! ❤️#WWDCWatchParty #WWDC20 pic.twitter.com/tfsX7PtKcO
— wwdcwatchparty (@wwdcwatchparty) June 28, 2020
I’m happy to be invited by Michie to talk along side with John and Łukasz, whom I really admire for their awesome open source contribution, to talk about how to start and contribute to open source
🥦 Bitrise webminar
During the 4th installment of our BUG #webinar series, our main focus was #iOSDev and #WWDC20. Let's recap what we discussed with our guests, @StuffMC, @axbotkin, @Moatazeldebsy, and @onmyway133 in our freshest blog post! ✨🔥https://t.co/rrA8cyYXus
— Bitrise (@bitrise) July 15, 2020
I shared my thoughts about my favorites at WWDC and the future ahead
🍇 Contributing.today
During Hacktoberfest 2020, I was contacted by my friend Maxim to share about open source
I had a pleasure to interview #OSS contributors from Norway @DrMowinckels and @onmyway133 for CONTRIBUTING project. October 2nd we kickoff #Hacktoberfest by online meetup about getting started with Open Source at 16:00 CEST. Join our sessions & workshops: https://t.co/hKkMKEOJJe https://t.co/1yr2AhQ4B6
— Maxim Salnikov (@webmaxru) October 1, 2020
I’m happy to continue another awesome year at Shortcut and DNB, where I have awesome and super nice colleagues. I ‘ve made lots of friends who I can talk with, who invited me to play badminton, tennis, football, swimming, hiking. Why didn’t we meet earlier?
I was lucky to attend a workshop hosted by John Sundell at work, I learned a lot
Ran my first ever large-scale remote workshop today, with over 25 participants, 10 different ongoing video calls for pair programming, and a ton of live coding 😀 Was so much fun!
— John Sundell (@johnsundell) August 27, 2020
Thanks a lot to the wonderful team at @shortcut_no for inviting me 😊 https://t.co/CE4P8Slo1G
Thanks for a memorable year, despite all the lockdown. And remember, balance work and life. You don’t need to be super rich to be happy. Having lots of money in the bank while living alone is not fun at all.
Another year is coming to an end. When looking back, do you miss the time you didn’t spend with your friends and family, or do you miss the time you didn’t get to do your work?
Updated at 2021-01-01 23:25:50
Issue #714
Below is an example of a parent ContentView
with State
and a child Sidebar
with a Binding
.
The didSet
is only called for the property that is changed.
When we click Button in ContentView
, that changes State
property, so only the didSet
in ContentView
is called
When we click Button in Sidebar
, that changes Binding
property, so only the didSet
in Sidebar
is called
1 | enum Tag: String { |
Another way to observe Binding changes is to use custom Binding
with get, set
. Here even if we click Button in ContentView
, the set
block is triggered and here we can change State
1 | var body: some View { |
We can also make convenient extension on Binding to return new Binding
, with a hook allowing us to inspect newValue. So we can call like Sidebar(tag: $tag.didSet
1 | extension Binding { |
Updated at 2021-02-06 21:57:28
Issue #713
To setup toolbar, we need to implement NSToolbarDelegate
that provides toolbar items. This delegate is responsible for many things
toolbarDefaultItemIdentifiers
itemForItemIdentifier
toolbarWillAddItem
and toolbarDidRemoveItem
1 | window.toolbarStyle = .unifiedCompact |
1 | extension NSToolbarItem.Identifier { |
Issue #712
Describe all possible paths in your program with enum. This is great to track down bugs and to not miss representing potential cases in UI. Errors can come from app layer, backend layer to network issues.
Enum is handy in both UIKit and SwiftUI
1 | enum NetworkError: Swift.Error { |
and note that Foundation has NSError
with a bunch of well defined error code ready to use. We can use this before introduce our custom error cases
1 | public var NSURLErrorUnknown: Int { get } |
Issue #711
List
has a selection parameter where we can pass selection binding. As we can see here selection
is of type optional Binding<Set<SelectionValue>>?
where SelectionValue
is any thing conforming to Hasable
1 | 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) (iOS |
So we can programatically control selection by tagging row with our own Tag
1 | struct SideView: View { |
Issue #710
Starting from macOS 11, we can use List
with SidebarListStyle
inside NavigationView
to declare master detail view. The SidebarListStyle
makes list translucent. It automatically handles selection and marks selected row in list with accent color.
1 | struct MainView: some View { |
If we use Section
instead of just Group we get a nice dropdown arrow button to expand and collapse section
1 | List { |
To toggle side bar, we can use toggleSidebar
selector since for now, sidebar is backed by NSSplitViewController
1 | mainWindow.firstResponder?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil) |
We can specify tool bar items on either sidebar or content.
1 | .toolbar{ |
For tool bar to work, we must use App
and embed views inside WindowGroup
1 | @main |
Updated at 2021-01-06 20:43:40
Issue #708
The best way to test is to not have to mock at all. The second best way is to have your own abstraction over the things you would like to test, either it is in form of protocol or some function injection.
But in case you want a quick way to test things, and want to test as real as possible, then for some cases we can be creative to mock the real objects.
One practical example is when we have some logic to handle notification, either showing or deep link user to certain screen. From iOS 10, notifications are to be delivered via UNUserNotificationCenterDelegate
1 | 10.0, *) (iOS |
and all we get is UNNotificationResponse
which has no real way to construct it.
1 | 10.0, *) (iOS |
That class inherits from NSCopying
which means it is constructed from NSCoder
, but how do we init it?
1 | let response = UNNotificationResponse(coder: ???) |
The trick is, since UNNotificationResponse
is NSObject
subclass, it is key value compliant, and since it is also NSCopying
compliant, we can make a mock coder to construct it
1 | private final class KeyedArchiver: NSKeyedArchiver { |
On iOS 12, we need to add decodeInt64
method, otherwise UNNotificationResponse
init fails. This is not needed on iOS 14
UNNotificationResponse
has a read only UNNotification
, which has a readonly UNNotificationRequest
, which can be constructed from a UNNotificationContent
Luckily UNNotificationContent
has a counterpart UNMutableNotificationContent
Now we can make a simple extension on UNNotificationResponse
to quickly create that object in tests
1 | private extension UNNotificationResponse { |
We can then test like normal
1 | func testResponse() throws { |
Another way is to build a proper KeyedArchiver
that checks key and return correct property. Note that we can reuse the same NSKeyedArchiver
to nested properties.
1 | private final class KeyedArchiver: NSKeyedArchiver { |
Updated at 2020-12-07 11:49:55
Issue #707
The trick is to set the button oinside of statusItem
to send actions on both leftMouseUp
and rightMouseUp
.
Another thing to note is we use popUpMenu
on NSStatusItem
, although it is marked as deprecated on macOS 10.14. We can set menu
but that overrides left click.
1 | import Omnia |
Updated at 2020-12-08 05:11:24
Issue #706
Use Mirror
and set key value as NSManagedObject
subclasses from NSObject
1 | import CoreData |
Updated at 2020-12-07 06:05:04
Issue #705
Supposed we have date in format ISO8601 and we want to get rid of T and millisecond and timezone Z
1 | const date = new Date() |
We can use toISOString
, then split base on the dot .
then remove character T
1 | date |
Issue #704
We used to declare enum that conforms to Error
, but any type like struct or class can conform to Error as well.
1 | enum NetworkError: Error { |
Issue #703
Read When to refresh a receipt vs restore purchases in iOS?
From iOS 7, every app downloaded from the store has a receipt (for downloading/buying the app) at appStoreReceiptURL
. When users purchases something via In App Purchase, the content at appStoreReceiptURL
is updated with purchases information. Most of the cases, you just need to refresh the receipt (at appStoreReceiptURL) so that you know which transactions users have made.
Note
SKReceiptRefreshRequest
or SKPaymentQueue.restoreCompletedTransactions
asks for Appstore credentialSKReceiptRefreshRequest
to download receipt from sandbox AppstorerestoreCompletedTransactions
updates app receiptrestoreCompletedTransactions
Users restore transactions to maintain access to content they’ve already purchased. For example, when they upgrade to a new phone, they don’t lose all of the items they purchased on the old phone. Include some mechanism in your app to let the user restore their purchases, such as a Restore Purchases button. Restoring purchases prompts for the user’s App Store credentials, which interrupts the flow of your app: because of this, don’t automatically restore purchases, especially not every time your app is launched.
In most cases, all your app needs to do is refresh its receipt and deliver the products in its receipt. The refreshed receipt contains a record of the user’s purchases in this app, on this device or any other device. However, some apps need to take an alternate approach for one of the following reasons:
More about receipt, from WWDC 2017, What’s new in StoreKit session https://developer.apple.com/videos/play/wwdc2017/303/
You can also watch WWDC 2017, session Advanced StoreKit for more detail https://developer.apple.com/videos/play/wwdc2017/305/
Read Restoring Purchased Products
Users sometimes need to restore purchased content, such as when they upgrade to a new phone.
Don’t automatically restore purchases, especially when your app is launched. Restoring purchases prompts for the user’s App Store credentials, which interrupts the flow of your app
In most cases, you only need to refresh the app receipt and deliver the products listed on the receipt. The refreshed receipt contains a record of the user’s purchases in this app, from any device the user’s App Store account is logged into
Refreshing a receipt doesn’t create new transactions; it requests the latest copy of the receipt from the App Store
Restoring completed transactions creates a new transaction for every transaction previously completed, essentially replaying history for your transaction queue observer. Your app maintains its own state to keep track of why it’s restoring completed transactions and how to handle them.
From AppStore
A consumable In-App Purchase must be purchased every time the user downloads it. One-time services, such as fish food in a fishing app, are usually implemented as consumables.
Non-consumable In-App Purchases only need to be purchased once by users. Services that do not expire or decrease with use are usually implemented as non-consumables, such as new race tracks for a game app.
Auto-renewable Subscriptions allow the user to purchase updating and dynamic content for a set duration of time. Subscriptions renew automatically unless the user opts out, such as magazine subscriptions.
Free subscriptions are a way for developers to put free subscription content in Newsstand. Once a user signs up for a free subscription, it will be available on all devices associated with the user’s Apple ID. Note that free subscriptions do not expire and can only be offered in Newsstand-enabled apps.
Subscription Non-Renewing Subscriptions allow the sale of services with a limited duration. Non-Renewing Subscriptions must be used for In-App Purchases that offer time-based access to static content. Examples include a one week subscription to voice guidance feature within a navigation app or an annual subscription to online catalog of archived video or audio.
Use this API to request a new receipt if the receipt is invalid or missing
Receipt is stored locally on device. It can be missing in case user sync or restore device.
Watch WWDC 2014 - 305 Preventing Unauthorized Purchases with Receipts
1 | Bundle.main.appStoreReceiptURL |
Read In-App Purchases: Receipt Validation Tutorial
The receipt consists of a single file in the app bundle. The file is in a format called PKCS #7. The payload consists of a set of receipt attributes in a cross-platform format called ASN.1
1 | case 12: // Receipt Creation Date |
Use TPInAppReceipt which includes certificates.
1 | try InAppReceipt.localReceipt() |
Check Receipt Fields
1 | Original Application Version |
Note
This corresponds to the value of CFBundleVersion (in iOS) or CFBundleShortVersionString (in macOS) in the Info.plist file when the purchase was originally made
CFBundleVersion
is build number, and CFBundleShortVersionString
is app version
1 | In-App Purchase Receipt |
Read Validating Receipts with the App Store
Sample verifyReceipt
json
1 | { |
Verify your receipt first with the production URL; then verify with the sandbox URL if you receive a 21007 status code. This approach ensures you do not have to switch between URLs while your application is tested, reviewed by App Review, or live in the App Store.
Let’s use enum to represent possible states for each resource. Here’s simple case where we only have 1 non consumable IAP product.
1 | enum IAPError: Error { |
Let’s have a central place for managing all IAP operations, called IAPManager
, it can update our ObservableObject
PricingPlan
hence triggers update to SwiftUI.
1 | import StoreKit |
You can use restoreCompletedTransactions
if you simply finishTransaction
and grant user pro feature, like in this simple tutorial In-App Purchase Tutorial: Getting Started, search for SKPaymentTransactionObserver
. restoreCompletedTransactions
also updates receipt.
Otherwise refreshing receipt is a better idea. It serves both case when receipt is not there locally and when you want to restore transactions. With receipt refreshing, no restored transactions are created and SKPaymentTransactionObserver
is not called, so we need to check receipt proactively.
Either restoreCompletedTransactions
or SKReceiptRefreshRequest
asks for AppStore credential so you should present a button there and ask user.
Try to locate local receipt and examine it.
originalAppVersion
in macOS and iOSIn practice, we need to perform some basic checks on receipt, like bundle id, app version, device id. Read In-App Purchases: Receipt Validation Tutorial, search for Validating the Receipt
. TPInAppReceipt
also has some handy verify
functions
Besides verifying receipt locally, it is advisable to call verifyreceipt either on device, or better on serve to let Apple verify receipt and returns you a human readable json for receipt information.
1 | func checkReceipt() { |
Finally, observe SKProductsRequestDelegate
which also conforms to SKRequestDelegate
for both product and receipt refresh request
1 | extension IAPManager: SKProductsRequestDelegate { |
Updated at 2020-12-04 06:58:17
Issue #702
The trick is to use an overlay
1 | MessageTextView(text: $input.message) |
Issue #701
Use mark. This does not work for multiline
1 | <p> |
Another way is to use background
1 | .highlight { |
Updated at 2020-11-20 05:23:59
Issue #700
In index.css
1 | body { |
Updated at 2020-11-18 06:29:29
Issue #699
Use term ZStack
like in SwiftUI, we declare container as relative position. For now it uses only 2 items from props.children
but can be tweaked to support mutiple
1 | class App extends React.Component { |
1 | /** @jsx jsx */ |
Updated at 2021-01-15 22:03:49
Issue #698
Xcode has powerful search. We can constrain search to be scoped in workspace, project or some folders. We can also constrain case sensitivity.
Another cool thing that people tend to overlook is, besides searching based on text, we can search based on references, definitions, call hierarchy, and 🎉 regular expressions.
Searching for regular expression gives us extra power when it comes to limit our search based on some criteria. For example when we are about to refactor some NSLayoutAnchor
code and we want to find all NSLayoutConstraint.
calls that stops at bottomAnchor
Here ‘s how to search NSLayoutConstraint
calls that involves bottomAnchor
1 | (NSLayoutConstraint(.*[\r\n])*).+?(?=bottomAnchor) |
1 | NSLayoutConstraint.activate([ |
Another tip when searching for regular expression is that we can use https://regex101.com/ to validate and fine tune our regex. Below are breakdowns of our regex. Note how we use /.+?(?=abc)/
to define “search until”
1 | / |
Updated at 2020-12-11 10:35:14
Issue #697
Use temporaryDirectory
from FileManager
and String.write
1 | func writeTempFile(books: [Book]) -> URL { |
Issue #696
Which methods do you think are used here
1 | import Cocoa |
The log is
1 | Init with a=1 and c=10 |