I have setup a Swift Package Manager with custom fonts for my iOS application.
I have three different types, and none of them are read and displayed in my SwiftUI app.
What is the proper way to setup and use a Swift Package Manager with custom fonts in it?
In my SPM, I have added my three different fonts, in a Font folder that I process in Package.swift. The font files are in the .otf format:
let package = Package(
name: "MyAwesomePackage",
platforms: [
.iOS(.v14)
],
products: [
.library(
name: "MyAwesomePackage",
targets: ["MyAwesomePackage"]),
],
targets: [
.target(
name: "MyAwesomePackage",
resources: [
.process("Fonts") // Processing my custom fonts
])
]
)
This is the code I am using in my Swift Package Manager to use my custom font in my SwiftUI app:
public extension Text {
// This is the modifier used in the SwiftUI app.
func customFont(_ type: CustomFontType) -> some View {
return self
.font(.custom(type.customFont,
size: type.size,
relativeTo: type.nativeTextStyle))
}
}
public enum CustomFontType {
case body
case header
case link
var customFont: String {
switch self {
case .header:
return "Gotham-Bold"
case .body:
return "Gotham-Book"
case .link:
return "Gotham-Medium"
}
}
var size: CGFloat {
switch self {
case .header:
return 32
case .body:
return 14
case .link:
return 12
}
}
var nativeTextStyle: Font.TextStyle {
switch self {
case .header:
return .largeTitle
case .body:
return .body
case .link:
return .footnote
}
}
}
And this is how I call my custom font in my SwiftUI app. The thing is that I do not have any of my font being populated when called with the .customFont(_:) modifier, and the font provided by the app is not even the Apple SFFont ones.
import MyAwesomePackage
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Custom Font")
.customFont(.header)
Text("Apple Font")
.font(.largeTitle)
.fontWeight(.bold)
}
}
Related
I did a small sample application to show my problem. I used the multi-platform document app template that Xcode 14.0.1 offers, creating my own package file format for this.
I want to create a document based app running on macOS and on iPad.
When running on macOS, everything works as expected.
On the iPad, when opening the app, the file chooser opens.
On opening an existing or creating a new file, the screen looks like this:
The left chevron does nothing, while the right chevron shows the document chooser again.
What's the left, ever so slightly larger chevron on the left doing here and how can I get of it? Is this an error with the framework that should be reported to Apple?
PS don't get distracted by the name of this sample app–the real app will need some navigation and I first thought the 2nd chevron show up cause of this–in the sample I built for this post, there is no navigation though. So this 2nd chevron seems to be a "built in" issue...
For the sake of completeness, here's my code:
import SwiftUI
#main
struct so_DocumentAppWithNavigationShowsMultipleChevronsApp: App {
var body: some Scene {
DocumentGroup(newDocument: so_DocumentAppWithNavigationShowsMultipleChevronsDocument()) { file in
ContentView(document: file.$document)
}
}
}
import UniformTypeIdentifiers
extension UTType {
static var appfroschFile: UTType {
UTType(importedAs: "ch.appfros.so-DocumentAppWithNavigationShowsMultipleChevrons")
}
}
struct so_DocumentAppWithNavigationShowsMultipleChevronsDocument: FileDocument {
var document: Document
init(document: Document = Document(text: "Hello, world!")) {
self.document = document
}
static var readableContentTypes: [UTType] { [.appfroschFile] }
init(configuration: ReadConfiguration) throws {
guard let fileWrappers = configuration.file.fileWrappers
else {
throw CocoaError(.fileReadCorruptFile)
}
guard let documentFileWrapper = fileWrappers["document"],
let data = documentFileWrapper.regularFileContents,
let string = String(data: data, encoding: .utf8)
else {
throw CocoaError(.fileReadCorruptFile)
}
document = try JSONDecoder().decode(Document.self, from: data)
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let data = try JSONEncoder().encode(document)
let documentFileWrapper = FileWrapper(regularFileWithContents: data)
let mainFileWrapper = FileWrapper(directoryWithFileWrappers: [
"document": documentFileWrapper
])
return mainFileWrapper
}
}
struct Document: Codable {
var text: String
}
struct ContentView: View {
#Binding var document: so_DocumentAppWithNavigationShowsMultipleChevronsDocument
var body: some View {
TextEditor(text: $document.document.text)
}
}
Can see the same problem with the default Document based app when using Xcode 14.1 (14B47) running on the iPad simulator with iOS 16.1. So definitely a bug (and worth reporting to A as such).
At a guess, the second, non-functional back button is what would have been the back button for navigating in SideBar. And the logic to not display when working on a document is what has been broken.
Fortunately simple workaround for the bug is to explicitly specify toolbar's role using the toolbarRole modifier, e.g.
#main
struct so_DocumentAppWithNavigationShowsMultipleChevronsApp: App {
var body: some Scene {
DocumentGroup(newDocument: so_DocumentAppWithNavigationShowsMultipleChevronsDocument()) { file in
ContentView(document: file.$document)
.toolbarRole(.navigationStack) // <-- Specifying this gets rid of double chevron on iOS
}
}
}
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
When creating a class conforming to ReferenceFileDocument, how do you indicate the document needs saving. i.e. the equivalent of the NSDocument's updateChangeCount method?
I've met the same problem that the SwiftUI ReferenceFileDocument cannot trigger the update. Recently, I've received feedback via the bug report and been suggested to register an undo.
Turns out the update of ReferenceFileDocument can be triggered, just like UIDocument, by registering an undo action. The difference is that the DocumentGroup explicitly implicitly setup the UndoManager via the environment.
For example,
#main
struct RefDocApp: App {
var body: some Scene {
DocumentGroup(newDocument: {
RefDocDocument()
}) { file in
ContentView(document: file.document)
}
}
}
struct ContentView: View {
#Environment(\.undoManager) var undoManager
#ObservedObject var document: RefDocDocument
var body: some View {
TextEditor(text: Binding(get: {
document.text
}, set: {
document.text = $0
undoManager?.registerUndo(withTarget: document, handler: {
print($0, "undo")
})
}))
}
}
I assume at this stage, the FileDocument is actually, on iOS side, a wrapper on top of the UIDocument, the DocumentGroup scene explicitly implicitly assign the undoManager to the environment. Therefore, the update mechanism is the same.
The ReferenceFileDocument is ObservableObject, so you can add any trackable or published property for that purpose. Here is a demo of possible approach.
import UniformTypeIdentifiers
class MyTextDocument: ReferenceFileDocument {
static var readableContentTypes: [UTType] { [UTType.plainText] }
func snapshot(contentType: UTType) throws -> String {
defer {
self.modified = false
}
return self.storage
}
#Published var modified = false
#Published var storage: String = "" {
didSet {
self.modified = true
}
}
}
ReferenceFileDocument exists for fine grained controll over the document. In comparison, a FileDocument has to obey value semantics which makes it very easy for SwiftUI to implement the undo / redo functionality as it only needs to make a copy before each mutation of the document.
As per the documentation of the related DocumentGroup initializers, the undo functionality is not provided automatically. The DocumentGroup will inject an instance of an UndoManger into the environment which we can make use of.
However an undo manager is not the only way to update the state of the document. Per this documentation AppKit and UIKit both have the updateChangeCount method on their native implementation of the UI/NSDocument object. We can reach this method by grabbing the shared document controller on macOS from within the view and finding our document. Unfortunately I don't have a simple solution for the iOS side. There is a private SwiftUI.DocumentHostingController type which holds a reference to our document, but that would require mirroring into the private type to obtain the reference to the native document, which isn't safe.
Here is a full example:
import SwiftUI
import UniformTypeIdentifiers
// DOCUMENT EXAMPLE
extension UTType {
static var exampleText: UTType {
UTType(importedAs: "com.example.plain-text")
}
}
final class MyDocument: ReferenceFileDocument {
// We add `Published` for automatic SwiftUI updates as
// `ReferenceFileDocument` refines `ObservableObject`.
#Published
var number: Int
static var readableContentTypes: [UTType] { [.exampleText] }
init(number: Int = 42) {
self.number = number
}
init(configuration: ReadConfiguration) throws {
guard
let data = configuration.file.regularFileContents,
let string = String(data: data, encoding: .utf8),
let number = Int(string)
else {
throw CocoaError(.fileReadCorruptFile)
}
self.number = number
}
func snapshot(contentType: UTType) throws -> String {
"\(number)"
}
func fileWrapper(
snapshot: String,
configuration: WriteConfiguration
) throws -> FileWrapper {
// For the sake of the example this force unwrapping is considered as safe.
let data = snapshot.data(using: .utf8)!
return FileWrapper(regularFileWithContents: data)
}
}
// APP EXAMPLE FOR MACOS
#main
struct MyApp: App {
var body: some Scene {
DocumentGroup.init(
newDocument: {
MyDocument()
},
editor: { file in
ContentView(document: file.document)
.frame(width: 400, height: 400)
}
)
}
}
struct ContentView: View {
#Environment(\.undoManager)
var _undoManager: UndoManager?
#ObservedObject
var document: MyDocument
var body: some View {
VStack {
Text(String("\(document.number)"))
Button("randomize") {
if let undoManager = _undoManager {
let currentNumber = document.number
undoManager.registerUndo(withTarget: document) { document in
document.number = currentNumber
}
}
document.number = Int.random(in: 0 ... 100)
}
Button("randomize without undo") {
document.number = Int.random(in: 0 ... 100)
// Let the system know that we edited the document, which will
// eventually trigger the auto saving process.
//
// There is no simple way to mimic this on `iOS` or `iPadOS`.
let controller = NSDocumentController.shared
if let document = controller.currentDocument {
// On `iOS / iPadOS` change the argument to `.done`.
document.updateChangeCount(.changeDone)
}
}
}
}
}
Unfortunatelly SwiftUI (v2 at this moment) does not provide a native way to mimic the same functionality, but this workaround is still doable and fairly consice.
Here is a gist where I extended the example with a custom DocumentReader view and a DocumentProxy which can be extended for common document related operations for more convenience: https://gist.github.com/DevAndArtist/eb7e8aa5e7134610c20b1a7aca358604
How to pass several preferences for given preference key?
var body: some View {
MyTabView {
Page1()
.preference(key: TabItemPreferenceKey.self, value: [TabItemPreference(tag: 0, item: AnyView(Text("Tab 1")) ) ] )
Page2()
.preference(key: TabItemPreferenceKey.self, value: [TabItemPreference(tag: 1, item: AnyView(Text("Tab 12")) ) ] )
Then in MyTabView I am trying to access this preferences but there is only first preference available
.onPreferenceChange(TabItemPreferenceKey.self) { preferences in
preferences.forEach { p in
self.items.tabItems.append((tag: p.tag, tab: p.item))
}
}
You can use
.transformPreference( _, _)
to combine multiple preferences
After some experimental debugging I've found the source of the problem. I've used .preference(key:) call on PageX() views inside MyTabView, but this views were conditionally displayed on screen! So .preference(key:) registers this preference only for displayed views.
I make a workaround to this issue using .hidden(). I only have concerns about performance as it would be better to display only needed page not add all pages at once and then hide them. But for now I do not know any other workaround.
Here is How I display this views to which I am attaching preferences:
var body: some View {
ForEach(0..<childs.count, id: \.self) { i in
Group {
if i == self.model.selection {
self.childs[i]
} else {
self.childs[i].hidden()
}
}
}
}
I'm trying to set a custom default font in my SwiftUI app. I tried several suggestions from this thread Set a default font for whole iOS app?.
However, none of those seem to work with SwiftUI. For example with this approach:
// Constants.swift
struct Fonts {
static var plex = "IBMPlexMono-Text"
}
// FontExtension.swift
extension UILabel {
var substituteFontName : String {
get { return self.font.fontName }
set { self.font = UIFont(name: Fonts.plex, size: 17)! }
}
}
// AppDelegate.swift in didFinishLaunchingWithOptions-function
UILabel.appearance().substituteFontName = Fonts.plex
When I start the app, the custom font appears for a split second and then changes back to the default font by Apple. Why does it change back to Apple's font and how can it be done permanently?
Is it maybe possible with an extension on Text-View?
You can have:
extension Font {
static let mediumFont = Font.custom("Sans-Regular", size: Font.TextStyle.subheadline.size, relativeTo: .caption)
static let mediumSmallFont = Font.custom("Sans-Regular", size: Font.TextStyle.footnote.size, relativeTo: .caption)
static let smallFont = Font.custom("Sans-Regular", size: Font.TextStyle.caption.size, relativeTo: .caption)
static let verySmallFont = Font.custom("Sans-Regular", size: Font.TextStyle.caption2.size, relativeTo: .caption)
}
extension Font.TextStyle {
var size: CGFloat {
switch self {
case .largeTitle: return 60
case .title: return 48
case .title2: return 34
case .title3: return 24
case .headline, .body: return 18
case .subheadline, .callout: return 16
case .footnote: return 14
case .caption: return 12
case .caption2: return 10
#unknown default:
return 8
}
}
}
and use it like this:
Text("Edit Profile")
.font(.mediumSmallFont)
this is the closest I could find to have a self-contained swift file that will change the font everywhere. make sure to call Font.setup in the app delegate on launch:
//
// Font.swift
// Hockey
//
// Created by #yspreen on 11/11/22.
//
import SwiftUI
extension Font {
static var brand = Font
.custom("Some Font", size: UIFont.preferredFont(
forTextStyle: .body
).pointSize)
static func setUp() {
let appearance = UINavigationBar.appearance()
let largeTitle = UIFont.preferredFont(
forTextStyle: .largeTitle
).pointSize
let body = UIFont.preferredFont(
forTextStyle: .body
).pointSize
let caption1 = UIFont.preferredFont(
forTextStyle: .caption1
).pointSize
print(UIFont.preferredFont(forTextStyle: .largeTitle))
appearance.largeTitleTextAttributes = [
.font : UIFont(
name: "SomeFont-Bold", size: largeTitle
)!
]
appearance.titleTextAttributes = [
.font : UIFont(
name: "SomeFont-Medium", size: body
)!
]
UITabBarItem.appearance().setTitleTextAttributes([.font: UIFont(name: "SomeFont-Regular", size: caption1)!], for: .normal)
UITabBarItem.appearance().setTitleTextAttributes([.font: UIFont(name: "SomeFont-Regular", size: caption1)!], for: .selected)
}
}
func Text(_ content: any StringProtocol) -> SwiftUI.Text {
.init(content).font(.brand)
}
func TextField(_ titleKey: LocalizedStringKey, text: Binding<String>, axis: Axis = .horizontal) -> some View {
SwiftUI.TextField(titleKey, text: text, axis: axis).font(.brand)
}
You can use a custom font like this:
Font.custom("Font-Family-Name", size: fontSize)
Example:
Text("Testing")
.font(Font.custom("Font-Family-Name", size: 16))
For using the font anywhere in the app, create a structure with as follows. Be sure to import SwiftUI in the file that will contain the font structure as:
import SwiftUI
struct AppFont {
static func commonFont(fontSize: CGFloat) -> Font {
return Font.custom("Font-Family-Name", size: fontSize)
}
}
Now you can use them anywhere in the app like this:
Text("Testing")
.font(AppFont.commonFont(fontSize: 16))