Check state of Option-Key in SwiftUI (macOS) - swiftui

I'm looking for a way to check the state of the option-key in SwiftUI on macOS.
I.e. depending on whether the option key is pressed or not I want to perform different actions in the .onTapGesture closure.

macOS-only SwiftUI has .modifiers modifier to specify EventModifiers, so your case is covered like in below example:
Rectangle()
.fill(Color.yellow)
.frame(width: 100, height: 40)
.gesture(TapGesture().modifiers(.option).onEnded {
print("Do anyting on OPTION+CLICK")
})
.onTapGesture {
print("Do anyting on CLICK")
}

While using the modifiers method on the gesture should probably be preferred, one can also actually test for the option key itself using CGEventSource in CoreGraphics:
import CoreGraphics
extension CGKeyCode
{
static let kVK_Option : CGKeyCode = 0x3A
static let kVK_RightOption: CGKeyCode = 0x3D
var isPressed: Bool {
CGEventSource.keyState(.combinedSessionState, key: self)
}
static var optionKeyPressed: Bool {
return Self.kVK_Option.isPressed || Self.kVK_RightOption.isPressed
}
}
This lets you detect the option key (or any other keys for that matter) in contexts where there isn't a modifier property or method.
The key codes in the extension can be renamed to be more Swifty, but those are the names that go way back to Classic MacOS's Toolbox and were defined in Inside Macintosh. I have a gist containing all the old key codes.

2022
.onTapGesture {
if NSEvent.modifierFlags.contains(.option) {
print("Option key Tap")
} else {
print("Tap")
}
}
.onDrag {
if NSEvent.modifierFlags.contains(.option) {
return NSItemProvider(object: "\(item.id)" as NSString)
}
return NSItemProvider()
}

Related

Initialize optional #AppStorage property with non-nil value

I need an optional #AppStorage String property (for a NavigationLink selection, which required optional), so I declared
#AppStorage("navItemSelected") var navItemSelected: String?
I need it to start with a default value that's non-nil, so I tried:
#AppStorage("navItemSelected") var navItemSelected: String? = "default"
but that doesn't compile.
I also tried:
init() {
if navItemSelected == nil { navItemSelected = "default" }
}
But this just overwrites the actual persisted value whenever the app starts.
Is there a way to start it with a default non-nil value and then have it persisted as normal?
Here is a simple demo of possible approach based on inline Binding (follow-up of my comment above).
Tested with Xcode 13 / iOS 15
struct DemoAppStoreNavigation: View {
static let defaultNav = "default"
#AppStorage("navItemSelected") var navItemSelected = Self.defaultNav
var body: some View {
NavigationView {
Button("Go Next") {
navItemSelected = "next"
}.background(
NavigationLink(isActive: Binding(
get: { navItemSelected != Self.defaultNav },
set: { _ in }
), destination: {
Button("Return") {
navItemSelected = Self.defaultNav
}
.onDisappear {
navItemSelected = Self.defaultNav // << for the case of `<Back`
}
}) { EmptyView() }
)
}
}
}
#AppStorage is a wrapper for UserDefaults, so you can simply register a default the old-fashioned way:
UserDefaults.standard.register(defaults: ["navItemSelected" : "default"])
You will need to call register(defaults:) before your view loads, so I’d recommend calling it in your App’s init or in application(_:didFinishLaunchingWithOptions:).

SwiftUI GKLeaderboard loadEntries

I would like to add leaderboards to my SwiftUI app.
I can't find any examples of using loadEntries to load leaderboard values.
I tried the following...
let leaderBoard: GKLeaderboard = GKLeaderboard()
leaderBoard.identifier = "YOUR_LEADERBOARD_ID_HERE"
leaderBoard.timeScope = .allTime
leaderBoard.loadScores { (scores, error) in ...
This results in the following warnings:
'identifier' was deprecated in iOS 14.0: Use
loadEntriesForPlayerScope:timeScope:range:completionHandler: instead.
'timeScope' was deprecated in iOS 14.0: Use
loadEntriesForPlayerScope:timeScope:range:completionHandler: instead.
'loadScores(completionHandler:)' was deprecated in iOS 14.0: Use
loadEntriesForPlayerScope:timeScope:range:completionHandler:.
using loadEntriesForPlayerScope results in the following warning:
'loadEntriesForPlayerScope(_:timeScope:range:completionHandler:)' has
been renamed to 'loadEntries(for:timeScope:range:completionHandler:)'
Using loadEntries I don't know how to specify the leaderboard identifier.
Here is simple demo of possible approach - put everything in view model and load scores on view appear.
import GameKit
class BoardModel: ObservableObject {
private var board: GKLeaderboard?
#Published var localPlayerScore: GKLeaderboard.Entry?
#Published var topScores: [GKLeaderboard.Entry]?
func load() {
if nil == board {
GKLeaderboard.loadLeaderboards(IDs: ["YOUR_LEADERBOARD_ID_HERE"]) { [weak self] (boards, error) in
self?.board = boards?.first
self?.updateScores()
}
} else {
self.updateScores()
}
}
func updateScores() {
board?.loadEntries(for: .global, timeScope: .allTime, range: NSRange(location: 1, length: 10),
completionHandler: { [weak self] (local, entries, count, error) in
DispatchQueue.main.async {
self?.localPlayerScore = local
self?.topScores = entries
}
})
}
}
struct DemoGameboardview: View {
#StateObject var vm = BoardModel()
var body: some View {
List {
ForEach(vm.topScores ?? [], id: \.self) { item in
HStack {
Text(item.player.displayName)
Spacer()
Text(item.formattedScore)
}
}
}
.onAppear {
vm.load()
}
}
}
I might be stating the obvious but have you looked at the WWDC20 videos?
Usually when there are big changes like this they cover it during WWDC that year.
Tap into Game Center: Leaderboards, Achievements, and Multiplayer
Tap into Game Center: Dashboard, Access Point, and Profile
I haven't looked at the videos but the documentation eludes that identifier might be replaced by var baseLeaderboardID: String

What is the best way to add ads to a SwiftUI Grid?

Hello I want to add ads to a swiftUI grid. The grid contains pictures that I get from a firebase backend and after every couple of pictures I would like to have an ad.
I am quite new to both SwiftUi and working with ads, so I'm not sure how correct my code is, but here is what I got so far.
// Code for the pictures Grid
struct PicturesGrid: View {
private let data: [Item]
var body: some View {
let gridItems = [GridItem(.fixed(UIScreen.screenWidth / 2),
alignment: .leading),
GridItem(.fixed(UIScreen.screenWidth / 2),
alignment: .leading)]
return ScrollView(showsIndicators: false) {
LazyVGrid(columns: gridItems) {
ForEach(0..<self.data.count, id: \.self) { index in
// Using this workaround for the ad to be on the whole width of the screen
// Also, after every six images I am adding and ad
if index != 0, index % 6 == 0 {
AdView()
.frame(width: UIScreen.screenWidth, height: 280)
.padding(.top, 20)
Spacer()
item
.frame(width: UIScreen.screenWidth / 2)
} else {
item
.frame(width: UIScreen.screenWidth / 2)
}
}
}
}
}
// this is for the picture
var item: some View {
NavigationLink(destination: DetailView(viewModel: DetailViewModel(item: itemAtIndexPath))) {
Cell(viewModel: CellViewModel(item: itemAtIndexPath))
}
.buttonStyle(PlainButtonStyle())
}
}
This is the code that I am currently using to load, create and display an ad
// Code for the ad that I am currently using
struct AdView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
let adController = AdViewController(self)
return adController
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
class AdViewController: UIViewController {
private var adView: AdView
/// The height constraint applied to the ad view, where necessary.
var heightConstraint: NSLayoutConstraint?
/// The ad loader. You must keep a strong reference to the GADAdLoader during the ad loading
/// process.
var adLoader: GADAdLoader!
/// The native ad view that is being presented.
var nativeAdView: GADUnifiedNativeAdView!
/// The ad unit ID.
let adUnitID = "ca-app-pub-3940256099942544/3986624511"
init(_ adView: AdView) {
self.adView = adView
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
var nibView: Any?
nibView = Bundle.main.loadNibNamed("ListAdView", owner: nil, options: nil)?.first
guard let nativeAdView = nibView as? GADUnifiedNativeAdView else {
return
}
setAdView(nativeAdView)
adLoader = GADAdLoader(adUnitID: adUnitID, rootViewController: self,
adTypes: [.unifiedNative], options: nil)
adLoader.delegate = self
DispatchQueue.global(qos: .background).async {
self.adLoader.load(GADRequest())
}
}
func setAdView(_ adView: GADUnifiedNativeAdView) {
// Remove the previous ad view.
DispatchQueue.main.async { [weak self] in
guard let weakSelf = self else {
return
}
weakSelf.nativeAdView = adView
weakSelf.view.addSubview(weakSelf.nativeAdView)
weakSelf.nativeAdView.translatesAutoresizingMaskIntoConstraints = false
// Layout constraints for positioning the native ad view to stretch the entire width and height
let viewDictionary = ["_nativeAdView": weakSelf.nativeAdView!]
weakSelf.view.addConstraints(
NSLayoutConstraint.constraints(
withVisualFormat: "H:|[_nativeAdView]|",
options: NSLayoutConstraint.FormatOptions(rawValue: 0), metrics: nil, views: viewDictionary)
)
weakSelf.view.addConstraints(
NSLayoutConstraint.constraints(
withVisualFormat: "V:|[_nativeAdView]|",
options: NSLayoutConstraint.FormatOptions(rawValue: 0), metrics: nil, views: viewDictionary)
)
}
}
}
extension AdViewController: GADUnifiedNativeAdLoaderDelegate {
func adLoader(_ adLoader: GADAdLoader, didFailToReceiveAdWithError error:
GADRequestError) {
print("didFailToReceiveAdWithError: \(error)")
}
func adLoader(_ adLoader: GADAdLoader, didReceive nativeAd: GADUnifiedNativeAd) {
print("Received unified native ad: \(nativeAd)")
// Deactivate the height constraint that was set when the previous video ad loaded.
heightConstraint?.isActive = false
// Populate the native ad view with the native ad assets.
// The headline and mediaContent are guaranteed to be present in every native ad.
(nativeAdView.headlineView as? UILabel)?.text = nativeAd.headline
nativeAdView.mediaView?.mediaContent = nativeAd.mediaContent
// This app uses a fixed width for the GADMediaView and changes its height to match the aspect
// ratio of the media it displays.
if let mediaView = nativeAdView.mediaView, nativeAd.mediaContent.aspectRatio > 0 {
heightConstraint = NSLayoutConstraint(
item: mediaView,
attribute: .height,
relatedBy: .equal,
toItem: mediaView,
attribute: .width,
multiplier: CGFloat(1 / nativeAd.mediaContent.aspectRatio),
constant: 0)
heightConstraint?.isActive = true
}
// This asset is not guaranteed to be present. Check that it is before
// showing or hiding it.
(nativeAdView.advertiserView as? UILabel)?.text = nativeAd.advertiser
nativeAdView.advertiserView?.isHidden = nativeAd.advertiser == nil
// In order for the SDK to process touch events properly, user interaction should be disabled.
nativeAdView.callToActionView?.isUserInteractionEnabled = false
// Associate the native ad view with the native ad object. This is
// required to make the ad clickable.
// Note: this should always be done after populating the ad views.
nativeAdView.nativeAd = nativeAd
}
}
I want to mention that this is working at the moment, but the problems that I want to fix and I don't know how are:
The grid with the pictures load, but when I scroll over an ad, it takes several seconds for the ad to load and display. How could I at least hide it while it loads or make it faster?
If I scroll over an ad, the ad loads and if I continue scrolling, when I scroll back up, the ad is not loaded anymore and I have to wait for it to load again. How can I fix this? Or what is the best practice for this kind of scenario?
Should I use multipleAds? To load them before showing? If yes, then how should I do this?
Does what I am doing here look even a little bit correct? Please...I need some help
The Best Way to show ads in SwiftUI Grids is implementing Native Ads in your app to provide personalized ad experience

Is it possible to set a character limit on a TextField using SwiftUI?

[RESOLVED]
I am using a codable struct which stores the object values retrieved from an API call so I have amended my TextField using Cenk Belgin's example, I've also removed extra bits I've added in so if anyone else is trying to do the same thing then they won't have pieces of code from my app that aren't required.
TextField("Product Code", text: $item.ProdCode)
.onReceive(item.ProdCode.publisher.collect()) {
self.item.ProdCode = String($0.prefix(5))
}
Here is one way, not sure if it was mentioned in the other examples you gave:
#State var text = ""
var body: some View {
TextField("text", text: $text)
.onReceive(text.publisher.collect()) {
self.text = String($0.prefix(5))
}
}
The text.publisher will publish each character as it is typed. Collect them into an array and then just take the prefix.
From iOS 14 you can add onChange modifier to the TextField and use it like so :
TextField("Some Placeholder", text: self.$someValue)
.onChange(of: self.someValue, perform: { value in
if value.count > 10 {
self.someValue = String(value.prefix(10))
}
})
Works fine for me.
You can also do it in the Textfield binding directly:
TextField("Text", text: Binding(get: {item.ProCode}, set: {item.ProCode = $0.prefix(5).trimmingCharacters(in: .whitespacesAndNewlines)}))

How to detect Light\Dark mode change in iOS 13?

Some of the UI setups not working automatically with the Dark/Light mode change as the UIColor. For example shadow in layer. As I need to remove and drop shadow in dark and light mode, I need somewhere to put updateShadowIfNeeded() function. I know how to detect what is the mode currently:
func dropShadowIfNeeded() {
switch traitCollection.userInterfaceStyle {
case .dark: removeShadow()
case .light: dropShadowIfNotDroppedYet()
default: assertionFailure("Unknown userInterfaceStyle")
}
}
Now I put the function inside the layoutSubviews, since it gets called every time appearance change:
override func layoutSubviews() {
super.layoutSubviews()
dropShadowIfNeeded()
}
But this function is getting called A LOT. What is the proper function to trigger only if userInterfaceStyle changed?
SwiftUI
With a simple environment variable on the \.colorScheme key:
struct ContentView: View {
#Environment(\.colorScheme) var colorScheme
var body: some View {
Text(colorScheme == .dark ? "Its Dark" : "Its. not dark! (Light)")
}
}
UIKit
As it described in WWDC 2019 - Session 214 around 23:30.
As I expected, this function is getting called a lot including when colors changing. Along side with many other functions for ViewController and presentationController. But there is some especial function designed for that has a similar signature in all View representers.
Take a look at this image from that session:
Gray: Calling but not good for my issue, Green: Designed for this
So I should call it and check it inside this function:
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) {
dropShadowIfNeeded()
}
}
This will guarantee to be called just once per change.
if you are only looking for the initial state of the style, check out this answer here
I think this should get called significantly less often, plus the guard makes sure you only react to user interface style changes:
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
guard previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle else {
return
}
dropShadowIfNeeded()
}
With RxSwift and ObjectiveC runtime, you can achieve it without inheritance
here is the encapsulated version:
import UIKit
import RxSwift
import RxCocoa
enum SystemTheme {
static func get(on view: UIView) -> UIUserInterfaceStyle {
view.traitCollection.userInterfaceStyle
}
static func observe(on view: UIView) -> Observable<UIUserInterfaceStyle> {
view.rx.methodInvoked(#selector(UIView.traitCollectionDidChange(_:)))
.map { _ in SystemTheme.get(on: view) }
.distinctUntilChanged()
}
}