Normal view

There are new articles available, click to refresh the page.
Before yesterdayMain stream

System fonts: the delights of San Francisco

By: hoakley
2 August 2024 at 14:30

Before Apple switched its system font to San Francisco in 2015, with El Capitan, it had used a succession of fonts already in widespread use, including Lucida Grande and Helvetica Neue. Those could be used in regular documents, embedded and used on other systems, and there was a good chance that anyone you sent documents to would also have those fonts installed locally, but not so for San Francisco, which is exclusive to Apple.

Open Font Book on a Mac with a standard suite of system fonts, and you won’t find San Francisco, or either of its abbreviated names of SF or SF Pro. Browse the list of fonts available in Pages or any other app and you won’t find them there either. The only place you can see them in cameo is in the 17 TrueType fonts of the SF family in /System/Library/Fonts, from SFArabic to SFNSRounded.

sysfonts1

Yet most apps use the system font in their interfaces, and sometimes they appear in their documents too. I’ll trace an example through my log browser Ulbow out to a PDF and web page of a log extract.

Because Ulbow browses log extracts, there must be no ambiguity in the glyphs displayed, no room for confusion between O and 0, or l and 1 (O and 0, or l and 1). It also helps if its characters are evenly spaced, so it uses the system monospaced font for the text contents of its window. Prior to Catalina, that was set using NSFont.monospacedDigitSystemFont(), but since then it’s done using NSFont.monospacedSystemFont() instead.

Ulbow then saves the styled text from a log extract, which is stored as an NSAttributedString, to Rich Text format. That’s much easier than it sounds, as that’s the standard format for such attributed strings, and conveniently supported by macOS. The Rich Text file sets the font to be used to .AppleSystemUIFontMonospaced-Regular, and that in turn is recognised correctly by my Rich Text editor DelightEd as the Regular face of the font .AppleSystemUIFontMonospaced. Provided that file is opened on a Mac, with the system fonts installed, macOS will find the font and display it correctly, even though you won’t ordinarily find any of these system fonts in app font menus.

Not all Rich Text editors will respect the system font set, though. While Nisus Writer Pro is happy to, Pages converts it to Helvetica whether it opens the RTF file direct, or the styled text is pasted in from DelightEd. Oddly, though, Pages adds the original font to its font menu so you can select the text and set it back to that font if you insist.

sysfonts2

Export that Rich Text file from DelightEd to PDF, and the font is carried through into the PDF source, where each block of text is set in the same font:
/Type /Font /Subtype /TrueType /BaseFont /AAAAAB+.SFNSMono-Regular /FontDescriptor
here named SFNSMono-Regular, the first mention of the system font filename. Note, though, that font isn’t embedded in the PDF, so when displayed on another system lacking macOS system fonts, it should be substituted using another monospace font such as Courier New.

sysfonts3

DelightEd will also export the Rich Text file to HTML using the standard built-in conversion, where embedded CSS sets the font as .AppleSystemUIFontMonospaced to maintain consistency. Unfortunately, that’s where the monospaced font comes to an end, as Safari won’t render it using that, despite running on macOS, and falls back to its default body font.

sysfonts4

These behaviours are all intended to ensure that San Francisco isn’t exported from your Mac to a non-Apple system. If you’re a registered Apple developer, you can download system fonts from Apple, and use them to design app mockups. But if you were to try transferring those to a different platform, then you would breach the font’s licence. In any case, other systems are likely to refuse to display those fonts because of the licence terms embedded in them.

There’s also a GitHub repository where you can download OpenType versions of all the San Francisco family for strictly non-commercial purposes. Each is accompanied by a summary of restrictions on its use.

Finally, the jewel in San Francisco’s crown isn’t a font at all, but a collection of thousands of vector-graphic symbols, SF Symbols. These are readily accessible in UIKit (iOS), AppKit (macOS), and above all in SwiftUI. A small selection of them appears in this screenshot from Unhidden.

unhidden2

Last Week on My Mac: Picking dates and times

By: hoakley
21 July 2024 at 15:00

When Apple offers us the first Macs with M4 chips they should please those who want more precise timing. As their CPU cores use the ARMv9.2-A instruction set architecture, they should feature 1 GHz timers, introduced in ARMv9.1-A. One of the surprises of the first three families of M-series chips has been their relatively coarse-grained time resolution: their Mach Absolute Time increments every 41.67 nanoseconds, while the M4 should be capable of 1 nanosecond increments, as supported by Intel CPUs.

If such momentary time intervals are hard to envisage, we’ve long been able to measure brief moments in time. Second hands on clocks were an occasional feature over four centuries ago, and became commonplace in the nineteenth century, when the advent of railways synchronised time across continents so that trains could run on schedule.

Date Pickers

Macs have multiple clocks, and a common way to access date and time using the Date structure from Foundation. Apple infers that this resolves to ‘sub-millisecond’ times, and Dates are commonly set down to the second. Look in Ulbow and some other of my apps and you’ll see the AppKit interface for this, in a Date Picker with NSDatePicker. Much of those date back to Mac OS X 10.4.

datepicker1

In this case, I set its style to textFieldAndStepper to let the user specify year, month, date, hours, minutes and seconds precisely. As with many other apps, second resolution is essential when working with the log, as a Mac can accumulate thousands or even tens of thousands of log entries in each second.

datepicker2

This has its equivalent in SwiftUI, using the same term of Date Picker, shown here in my prototype log browser LogUI. What’s missing here are the seconds, which are only allowed in watchOS.

In SwiftUI, a Date Picker comes with set components, and those including seconds, hourMinuteAndSecond, aren’t available in macOS, iOS or iPadOS, only in watchOS. The others have to make do with hourAndMinute without seconds, which explains why I’ve had to tag on a separate view to handle seconds.

Not only is there no support for entering seconds in the SwiftUI Date Picker for macOS, but times set using it pick their own value for seconds, which appears to be random, rather than fixing the seconds value at zero. Thus, its value in any Date entered using SwiftUI could range between 0 and 59.

Using AppKit, obtaining the Date set using its Date Picker is simple: for a picker of
@IBOutlet var textBaseDate: NSDatePicker!
the code
let startDate = self.textBaseDate.dateValue
returns the date and time set, including seconds, in startDate.

Adding support for seconds

Dates are opaque structures, and don’t give direct access to components within them such as seconds. To cater for SwiftUI’s shortcomings, more code is required. In the view, the Date obtained from the Date Picker has to be supplemented by a separate value for seconds:
@State private var theStartDate = Date()
@State private var theSecs = "00"
DatePicker("Start", selection: $theStartDate, displayedComponents: [.date, .hourAndMinute])
TextField(":", text: $theSecs)

sets theStartDate to the date with uncontrolled seconds, and theSecs to the number of seconds entered separately by the user.

Then the code that incorporates the seconds into the Date has to work through an intermediate Calendar to do so. It first deconstructs the Date into its components:
let calendar = Calendar(identifier: .gregorian)
var components = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: date)

then sets the seconds correctly, and reconstructs the corrected Date:
components.second = theDateSecs
let dateWithSecs = calendar.date(from: components)!

SwiftUI

SwiftUI was first released for macOS Catalina and iOS 13 in 2019, so celebrated its fifth birthday last month. That makes it only two years younger than APFS, but the contrast in maturity between them could hardly be greater. AppKit may have the advantage of age, in that it was released with the first Mac OS X in 2001, but by the time it was five, midway between 10.4 Tiger and 10.5 Leopard, it was both extensive and mature. AppKit didn’t face the challenge of being cross-platform, though, and parallels with UIKit in iOS came later.

SwiftUI has also rushed forward in novel fields, laying greatest emphasis on iOS. Without sound foundations, though, it risks failure. If it still can’t perform basic functions long-supported by AppKit, then few macOS apps will adopt it, and it will stay playing second fiddle. That would be a bad outcome for all those involved, not least users expecting consistency and high quality in the human interface across Macs and Apple’s devices.

Any good five year-old should be able to tell the time including seconds. Yet SwiftUI hasn’t yet caught up with those German clockmakers of the sixteenth century and their second hands, and seems stuck in the age of sundials. How can it possibly cope with the first Macs based on M4 chips with their nanosecond resolution?

Writing a third-generation log browser using SwiftUI: 2 Displaying log entries

By: hoakley
16 July 2024 at 14:30

In the first article explaining how I’m developing my third-generation log browser, I considered how it obtains its log entries using the OSLog API in macOS. Those end up in a structure containing the individual contents of each of the fields in a log entry. Here I explain how those entries are displayed to the user, with SwiftUI.

There are several options for displaying log entries. Console adopts a column view that really doesn’t work well even on a 27-inch display.

logui00

Experience developing Ulbow has shown that use of semantic colour to distinguish fields in log entries is superior.

ulbow1b103

Rather than confine fields to columns of even width, and leave several of them empty according to the fields used in that type of log entry, I have therefore opted for a display based on that in Ulbow.

logui01

This is most effective in Dark mode.

Styled text

It’s common for log browsers like Ulbow to be called on to display thousands of log entries, even when predicates are used by experts. Ulbow accomplishes that using the building block for Rich Text, NSAttributedString. Each field has a colour attribute applied to it, and those are appended to one another to build the line representing each log entry. Those lines are then appended to one another to assemble the full NSAttributedString to be displayed in the text scroller forming the lower section of Ulbow’s document window, using AppKit.

The equivalent using SwiftUI uses the almost identically named AttributedString, then displayed as a Text view in a SwiftUI Scroller.

My first experiment was therefore to repeat the same process using AttributedStrings instead of NSAttributedStrings. In code:
let theProcess = self.setAttrStr(string: (log.process + " "), color: NSColor.green)
theLineStr.append(theProcess)

becomes
var theProcess = AttributedString(log.process + " ")
theProcess[AttributeScopes.AppKitAttributes.ForegroundColorAttribute.self] = .green
theLineStr.append(theProcess)

to colour the Process name in green and append it to the log entry.

Working entirely using the newer AttributedString, this proved unusable. Even displaying just a couple of hundred log entries in a single AttributedString took several seconds, and when there were 1,000 or more the whole window was unusable after taking 15 seconds to open.

loguiTextASRun

When assessed using Instruments, a small test log excerpt took over 1.6 seconds to open, little of that taken by the view itself, but much of that period apparently being used by the CPU, presumably handling the AttributedString.

The code was refactored to compare the following variations:

  • creating the AttributedString one field at a time, appending those in a single entry into one Attributed String, then appending that to the whole AttributedString to be displayed;
  • creating an NSAttributedString for each entry, converting that to an AttributedString, then appending that to the whole AttributedString to be displayed;
  • creating each entry as an NSAttributedString, appending those into a single NSAttributedString, and converting that into an AttributedString for display.

There was no significant difference in their performance, so I abandoned further attempts to display log entries as styled text in a SwiftUI Text Scroller.

Styled list

The other obvious choice for display of log entries is SwiftUI’s List, as I had already used in less demanding lists such as that in Unhidden. This approach iterates through each log entry when building the view, styling each field as it goes, for example in:
if !line.process.isEmpty {
if #available(macOS 14.0, *) {
Text(line.process)
.foregroundStyle(.green)
} else {
Text(line.process)
.foregroundColor(.green)
}}

This has to contain two alternative calls to set the foreground colour of the text, as foregroundStyle() is required for macOS 14 and later, while foregroundColor() is required in older versions of macOS. Unfortunately, because of its rapid change, SwiftUI is riddled with such version conflicts.

To my surprise, this List approach performed much better than using AttributedString, and windows typically open in less than a second even when there are more than 1,000 log entries to be displayed.

loguiListRun

This is demonstrated again by Instruments, here opening a log window of the same size as shown above for the Text Scroller implementation. The whole process took 0.44 seconds, most of which was in handling the view. Memory use was also substantially less than for AttributedString. This scales well, with 10,000 log entries remaining brisk, and even 20,000 being perfectly usable.

This is how log entries are displayed in LogUI (to rhyme with doggy), available as a notarized app from here: logui01

Source code for the List view is given in the Appendix at the end of this article.

Copy and save

Unlike similar views in AppKit, List views have little or no intrinsic support for common features, such as selection and copying of content, or saving in Rich Text format, a feature that also appears to have been omitted from AttributedString, although there’s helpful support provided for NSAttributedString.

Having proved the concept of obtaining log entries using OSLog rather than the log show command, and displaying those entries in a SwiftUI List view, I’m now refactoring this into a document-based app, which should at least facilitate saving log extracts in Rich Text format.

Reference

SwiftUI List view in Apple Developer Documentation

Appendix: Source code

struct ContentView: View {
    @State private var messageInfo = Logger()
    
    var body: some View {
        let _ = messageInfo.getMessages()
        if (self.messageInfo.logList.count > 0) {
            VStack {
                List(self.messageInfo.logList) { line in
                MessageRow(line: line)
                }
            }
            .frame(minWidth: 900, minHeight: 200, alignment: .center)
        } else {
            Text("No results returned from the log for your query.")
                .font(.largeTitle)
        }
    }
}

struct MessageRow: View {
    let line: LogEntry

    var body: some View {
        HStack {
            Text(line.date + "  ")
            if #available(macOS 14.0, *) {
                Text("\(line.type)")
                    .foregroundStyle(.red)
            } else {
                Text("\(line.type)")
                    .foregroundColor(.red)
            }
            if !line.activityIdentifier.isEmpty {
                Text(line.activityIdentifier)
            }
//          etc.
            Text(line.composedMessage)
            }
        }
    }

Writing a third-generation log browser using SwiftUI: 1 Getting log entries

By: hoakley
15 July 2024 at 14:30

The most difficult feature of macOS is getting to grips with its log. I know developers of the highest calibre and advanced users who simply can’t make any sense of it, and I’ve heard of researchers who’d rather disassemble code than try reading the log.

My first attempt to open access to the Unified log was an AppleScript-based app named LogLogger, released here a month after Apple unleashed the new log system on us in Sierra, nearly eight years ago.

loglogshot3

That developed into Consolation, written in Swift 3 and released in its first beta four months later.

consolscript20

With Consolation in version 3, at the end of 2019, I released the first beta of Ulbow, with its cleaner interface, filters and colour formatting of log entries, the second generation.

ulbow1b103

Over those years, each app has relied on calling the log show command and parsing resulting extracts in JSON. This is largely because Apple didn’t provide an API with direct access to read the Unified log until macOS Catalina five years ago. That API is Spartan in comparison with the riches of log show, and appears to be little-used or exemplified. The time has come now to switch from parsing JSON to grappling direct with macOS, in a third generation of log utilities based on OSLog.

Get log entries with OSLog

On the face of it, getting entries from the log is straightforward using OSLogStore.getEntries(). This takes two important parameters: an OSLogPosition indicating the starting point for the sequence of entries, and an NSPredicate to be used to filter those entries. An outline sequence of calls is:
let store = try OSLogStore.local()
to select the local active log store, rather than a logarchive saved previously.
entries = try store.getEntries(with: [], at: secsInterval, matching: pred)
where secsInterval is an OSLogPosition and pred is an NSPredicate, or nil to fetch all log entries. This returns a sequence of OSLogEntry entries.

A quick glance at the documentation for OSLogEntry, though, shows that it contains only three fields out of the many accessible in each log entry:

  • composedMessage, a string containing the message field for that entry
  • date, a Date containing the timestamp of that entry
  • storeCategory, an enumeration setting how long that entry should be retained in the log.

It looks as if OSLog provides only very basic access to the log, until you consider the other classes descended from OSLogEntry. Together with two protocols, these actually expand to support five different types of log entry, of which three contain many fields. This is because different classes of log entry contain different fields, as shown in the diagram below.

logentries1

In any sequence of entries, most will be OSLogEntryLog, with many Signposts of OSLogEntrySignpost class holding 15 fields in all. OSLogEntryBoundary are least frequent, and only likely to be encountered in the announcement at the start of the kernel boot process.

To separate these out from the sequence of entries returned by OSLogStore.getEntries(), you therefore have to determine which class each entry belongs to, with code such as
for ent in entries {
if let log = ent as? OSLogEntryLog {

// process OSLogEntryLog class
} else if let log = ent as? OSLogEntryActivity {
// process OSLogEntryActivity (Activity) class
}
and so on.

To store extracted field data for each log entry, I thus create a LogEntry structure with a variable to contain data for each of the fields available, and each entry is used to complete one of those structures in a sequence. That and my code are provided in the Appendix at the end of this article.

Time period

The next challenge in accessing the log using OSLog is specifying the time period for which you want entries. This appears enigmatic as OSLogStore.getEntries() doesn’t allow you to specify two time points, merely a location in the log as an OSLogPosition, which comes in three flavours:

  • a specified date and time point
  • a time interval since the latest log entry
  • a time interval since the last boot

Using any of those three, passed as a parameter to OSLogStore.getEntries(), entries will continue until they reach the end of the log. To illustrate how to use these in practice, here are two examples.

If you want log entries between 04:31:15 and 04:32:30, you can obtain the OSLogPosition for 04:31:15, getEntries() using that, check the timestamp on each log entry until the first for 04:32:30 appears, then return out of the iteration. Alternatively, you could set the OSLogPosition at the end and use the OSLogEnumerator.Options to iterate in reverse (although others have reported that doesn’t work).

If you want the last 12 seconds of log entries, you can set the TimeInterval since the latest log entry to -12.0 seconds, and use the OSLogPosition to start iterations 12 seconds ago. Here you’ll want to keep track of the total entries returned and perhaps limit that to 1,000 or 10,000, or your code might blow up somewhere, and that’s simple to do using a loop count and return.

Predicates

Those difficulties pale in comparison to the last of the parameters to OSLogStore.getEntries(), the predicate. This is simplest when you want all entries, as you then pass nil. For this article, I’ll give the next simplest solution, a single predicate specifying the subsystem. This can be generated using
let pred = NSPredicate(format: "subsystem == %@", predicate)
where predicate is a String containing the name of the subsystem, such as com.apple.Bluetooth.

LogUI

I have now wrapped this in a simple SwiftUI interface as a demonstration, in LogUI (to rhyme with doggy), available as a notarized app from here: logui01

logui01

This gives you control over three parameters:

  • a subsystem to use as a predicate
  • the number of seconds to go back from the current end of the log, to start collecting from, as explained above
  • the maximum number of log entries to display

as configured in its Settings. Its single-page Help file explains the colour code used to display extracts.

In the next article I’ll consider how best to display log extracts using SwiftUI, and how I arrived at the solution in LogUI.

Reference

OSLog, Apple Developer Documentation

Appendix: Source code


struct LogEntry: Identifiable {
    let id = UUID()
    var type: Int = 0
    var activityIdentifier: String = ""
    var category: String = ""
    var composedMessage: String = ""
    var date: String = ""
    var level: String = ""
    var parentActivityIdentifier: String = ""
    var process: String = ""
    var processIdentifier: String = ""
    var sender: String = ""
    var signpostIdentifier: String = ""
    var signpostName: String = ""
    var signpostType: String = ""
    var storeCategory: String = ""
    var subsystem: String = ""
    var threadIdentifier: String = ""
}
    func getMessages() -> Bool {
       var predicate = UserDefaults.standard.string(forKey: "predicate") ?? ""
       let period = UserDefaults.standard.string(forKey: "period") ?? "-5.0"
       let maxCount = UserDefaults.standard.string(forKey: "maxCount") ?? "1000"
       let theMaxCount = Int(maxCount) ?? 1000
       var theSecs = Double(period) ?? -5.0
       if theSecs > 0.0 {
           theSecs = -theSecs
       }
       let dateFormatter = DateFormatter()
       dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSSZ"
       do {
            let store = try OSLogStore.local()
            var theCount = 0
            let secsInterval = store.position(timeIntervalSinceEnd: theSecs)
            var entries: AnySequence<OSLogEntry>
            logList = []
            if !predicate.isEmpty {
                let pred = NSPredicate(format: "subsystem == %@", predicate)
                entries = try store.getEntries(with: [], at: secsInterval, matching: pred)
            } else {
                entries = try store.getEntries(with: [], at: secsInterval, matching: nil)
            }
            for ent in entries {
                var theLineEntry = LogEntry()
                if let log = ent as? OSLogEntryLog {
                    theLineEntry.type = 1
                    theLineEntry.category = log.category
						 // etc.
                } else if let log = ent as? OSLogEntryActivity {
						 // etc.
                } else {
                    let log = ent as OSLogEntry
                    theLineEntry.type = 10
                    theLineEntry.composedMessage = log.composedMessage
                    theLineEntry.date = dateFormatter.string(from: log.date)
                    theLineEntry.storeCategory = "\(log.storeCategory.rawValue)"
                }
                if (theLineEntry.type != 4) && (theLineEntry.type != 0) {
                    logList.append(theLineEntry)
                    theCount += 1
                }
                if theCount > theMaxCount {
                    return true
                }
            }
            return true
        } catch {
            return false
        }
    }

预言在应验:五年前所讨论的未来人机交互的新范式_6.ylog

By: Steven
16 June 2024 at 22:58

从 2024 年的今天,回望 2019 年的 Apple 和 Ive 团队,我们会发现有些变化和趋势似乎是早已注定的。在过往的观察和分析中,我们所预言的事情正在成为现实和主流。常言道以史为镜可以知兴替,今天再看当时的 Apple 和 Ive 团队,关于产品的演进思路和设计策略的变化都早有端倪,也能预见在 AI 席卷的浪潮下,Apple 将会如何应对。

在这一期,你会听到:

—- 二十年前的专利文件:通体透光的 iPhone

—- 国产厂商和 Apple 在设计上的差异

—- 成功的设计:AirPods 只是剪掉线的 EarPods

—- 塑料手机的设计巅峰:iPhone 5c

—- 刘海与机器视觉:早早布局的 AI 伏笔

—- 未来十年的人机交互:人和人之间怎么交互?

—- 设计策略上的「S型曲线」体现在哪里?

—- 产品路径上迷路的 iPad

—- 光洁的划痕:是矫情还是哲学?

—- 史上最佳手机壳:iPhone 5c 的多彩硅胶壳

—- 拟物化的残党,现在理解扁平化的先进性了吗?

|相关图片|

首款 Unibody 设计于 2008 年 10 月发布
截图来自:Designed by Apple in California

查看更多图片和设计讨论:Mac Pro 2019

|拓展阅读|

如何评价 iPhone X 的工业设计?

交互的王,时代的狂!万字详解灵动岛的今生来世!

十年轮回?经典进化!工业设计师深入解读 iPhone12!

从技术寿命 S 曲线,看阳极氧化铝的设计

抽象的产品,用户「界面」的设计

如何看待 Evans Hankey 从 Apple 设计团队离职?

注定会离职的 Jonathan Ive 和科技产品的设计趋势

|登场人物|

苏志斌:工业设计师,车联网智能硬件产品经理 / 联创,《设以观复》作者

王汉洋:AI 行业从业者,多档播客主播,《拯救东北1910》《山有虎》作者

|相关链接|

若你所使用的播客客户端未能完整显示插图,或遇网络问题未能正常播放,请访问:

荒野楼阁 WildloG 的地址:https://suithink.me/zlink/podcast/

阅读设计相关的各类文章:https://suithink.me/zlink/idea/

|其他社交网络媒体|

苏志斌 @ 知乎|SUiTHiNK @ 即刻 / 微博

苏志斌SUiTHiNK @ Bilibili / YouTube / 小红书

|联络邮箱|

suithink.su@gmail.com

欢迎在 小宇宙、Spotify、YouTube、Apple Podcast 收听本节目,期待你的留言。

面向产品设计的 Web 前端分享会(分享会记录)

By: 李瑞东
2 July 2023 at 18:04

最近听说部门里面的产品或本地化运营对 Web 前端相关的内容比较感兴趣,正好我有相关的实践经验,所以在公司做了一个 Web 前端相关的分享会。分享内容包含:

  1. 使用 Devtools:介绍 Chrome 浏览器内的模拟、编辑和审查工具;
  2. 网页和部署:介绍 HTML, CSS, JavaScript, React,以及网站的部署和托管;
  3. 网页性能指标:介绍网页性能常用指标和测量工具;
  4. 资源分享:分享浏览器插件、网站和课程推荐。

与以往不同的是,这次分享会中加入了互动环节。我做了一个代码 Playground,尝试帮助观众了解 React,以及 React Props 的概念,并留了两个小任务,给观众尝试去实践对 React 项目进行编码。

完整的分享内容内容请继续浏览本文。

使用 Devtools

这个章节主要介绍 Chrome Devtools 一些可能不为人知的功能,来帮助我们提高日常工作中的效率和解决一些问题。先介绍 Devtool 里面「模拟」相关的功能。

模拟设备和屏幕尺寸

在 Devtool 里打开设备工具栏,在这里除了能够自由调整网页宽高,还能够模拟各种主流设备的屏幕。

甚至还能读取到网页里面的断点样式,提供快捷切换各种断点的方式。

需要注意的是,这里模拟的设备是会带上 UA 的,所以如果想在电脑里调试一些做了移动端特化处理的网站(比如访问主域名时,判断到是手机设备,则会跳到移动端的专门网站),是需要用到这个功能的。

模拟伪类

Devtools 还可以帮助我们排查各种交互状态下的样式问题,最常用的是,比如说我们想仔细排查某个元素的悬停和按下状态的样式,则可以在选中元素之后,勾选对应的伪类选项。

模拟媒体

在渲染面板(需要手动开启,浏览器默认是没有打开这个面板的)能够模拟部分系统设置,比如亮暗模式、打印模式、高对比度和减少动态效果等。

与之对应地,可以扩展一个概念叫做 CSS 的媒体查询,CSS 还可以探测到很多用户设备的属性或者设置,比如设备指针精度、视窗比例、当前是否全屏模式、设备方向等…

能探测的内容很多,但实际能用起来的可能只有寥寥数个,最全面的信息可以取 MDN 上查看。

编辑网页文字样式

Devtools 还提供了一个新的字体编辑器,能够让我们实时更改网页中的字体家族、字体大小、字重等属性。

编辑网页内容

我们在 Devtools 控制台里面执行代码document.designMode = 'on' 后,就可以实时在本地修改网页文字内容了,就跟平常打字一样。很适合用在测试文案长度的场景。最后也会分享一个浏览器插件,能够对网页做更多的编辑。

审查 React 组件

最后介绍一个审查 React 组件的方法,有时候我们想看某个元素是不是用的组件库,或者这个组件包含了什么属性之类的,可以下载 React Developer Tools,然后点选网页中的任意元素,进行审查。

网页和部署

接下来我介绍一下网页构成和网站部署相关的内容。

通常来说,HTML, CSS, JavaScript 是构成网站的三个要素。其中:

  • HTML 用来用于定义网页的结构和内容,可以用来创建网站的各个部分,比如标题、段落、图片、链接等。
  • CSS 用来定义网页的样式和布局,这个可能会是咱们设计师比较熟悉的部分,我们能够利用 CSS 来定义 HTML 元素的各种样式,控制它们的布局和位置。
  • JavaScript 用来实现各种功能逻辑和操作交互。比如响应点击事件、动态修改网页内容,根据条件执行动画效果或展示特定内容等。

CSS 预处理器

上述的三种语言,都有各自对应的语法规则。而 CSS 预处理器,则改进了原有的 CSS 语法,使我们能使用更复杂的逻辑语法,比如使用变量、代码嵌套和继承等。

简单来说,CSS 预处理器能让我们写样式代码的过程更顺畅,使代码有更良好的可读性和扩展性,帮助我们更好地维护代码。

举个简单的例子,比如原本的 CSS 语法中要求我们给每一个元素写样式时,必须以花括号开始和结尾,而且每一条样式规则直接都要以分号隔开,而 Stylus 则能够让我们跳出这个限制。直接用换行和缩进来代替。

CSS 框架

另一个值得一提的概念是 CSS 框架。CSS 框架则提供了一套预设样式,比如颜色板、字体梯度,布局和断点设定等;以及一些常用组件,如导航栏、对话框和页脚等。

简单来说,就是提供了一批开箱即用的样式,便于开发者快速启动项目,同时也会保留高度自定义的空间,用于支持各种各样的需求。通常 CSS 框架都会包含使用某个 CSS 预处理器,甚至内置了一些图标库,主打一个 “开箱即用”。

这里稍微介绍一下一个 CSS 框架:Tailwind CSS。是一个高度定制化的 CSS 框架,通过大量的预定义类名,使开发人员快速构建和设计网页界面。

与其他 CSS 框架相比,有一个显著的特点是 Tailwind CSS 本身不会包装一个组件出来,比如按钮、输入框的样式,没有预设好的。取而代之的是,Tailwind CSS 将各种原子级的 CSS 类名包装起来,比如:

  • 设置左右两边的 Padding,用 px-[...] 类名来实现;
  • 设置一个元素为块级元素, 用block 类名来实现…

如果想要在 TailwindCSS 中,使用打包好的组件,达到开箱即用的效果,可以通过各种官方/非官方的模版或组件生态来进行。比如:

React

接下来介绍另一个概念:React。这是一个用于构建 Web 和原生交互界面的库(是的,它能够用来做 App,不仅仅是网页)。而且引入了 JSX 语法,将 HTML 和 JS 结合起来,以一种更直观和易于理解的方式描述界面的结构和内容。

React 有一点和我们的设计稿很像,就是它的组件思维。在构建用户界面时,React 主张先把内容分解成一个个可以复用的组件(把具体的交互、功能逻辑写在组件里面)。然后在页面中将各个组件连接起来,使数据流经它们。

下图引用了官网中的一个例子,其中:

  1. 完整的应用,可以理解为由多个组件拼接成的完成网页;
  2. 搜索组件,用来获取用户输入;
  3. 数据列表,会根据用户搜索来过滤和展示内容;
  4. 每个列表的表头;
  5. 表格内每条数据。

现在我们用一个具体例子来简单介绍下 React 的组件。

在上图中,展示了一个页面页面 App.jsx 包含了 Profile、Gallery 和 FAQ 组件,以及 Profile.jsx 组件的代码。右侧是输出页面,展示了三个组件拼接而成的页面效果示意图,其中 Profile 组件模块里展示的内容,是和 Profile.jsx 文件内代码一一对应的。

上述的组件只是将一个模块包装起来,使其能够被其他地方复用。但组件内容是固定的。接下来会为大家展示如何向组件传递 Props,实现上文提到的一句话 “使数据流经他们” 。

在上图中,我们先将一些 Props 传递给组件 Profile(比如这里传递了图片的地址、人物姓名和描述),然后在 Profile 组件内接收这些 Props,并在组件代码内使用这些数据。

现在,我们就做出了一个可以复用的组件了,可以根据不同的内容来展示相关的人物信息。

大家有没有觉得这种做法有点熟悉?是的,在 Figma 中,我们的组件里面也有类似的做法。Figma 组件同样同样传递字符串、布尔和组件等内容。

实际上 React 组件可以传递的参数不仅仅只是上面例子中的字符串和布尔值,还能传递数值、函数、对象、Node 类型甚至另一个组件等。

我做了一个简单的 Playground,提前封装好了一个 Profile 组件,会传递一些字符串、布尔值(是否展示网站标签)以及数值(圆角大小),帮助大家更好地理解。

🛝 Playground

我做了一个 🛝 Playground ,大家可以在里面看到这个组件的具体的情况,实际看一遍代码可能会帮助理解 React 的组建和 Props 概念。

同时我也写了两个小任务给到大家去尝试,大家可以在上面的编辑器中自由尝试。

发布网站

到了这里,相信大家对构建一个网站已经有了初步的认识,接下来我为大家介绍下如何将构建好的网站发布的互联网当中,能够真正地被世界各地的人们浏览。

方法一:部署到服务器

这是比较传统的方法,先将项目相关的文件放进服务器里面(比如阿里云 ECS,或轻量服务器等)。然后在服务器内安装 NGINX,索引到项目文件夹,定义好首页、端口、404 等场景,最后将域名解析到服务器 IP。之后我们的网站就能在互联网上被人们访问了。

方法二:托管到服务商

这种是相对省心的方法,将我们项目所在的 GitHub 仓库,链接到服务商的托管服务当中。等于是由服务商来帮我们部署、发布项目,不用自己来配置服务器的各种内容了。下图列举了几种常见的网站托管服务商,分别是:Vercel,Github Pages 和 Netlify。

以 Vercel 来举例,除了能够托管网站之外,对每一次发布进行管理,甚至能够是对不同代码分支进行独立发布,还能收集网站访问数据等。

网页性能

接下来为大家介绍网页性能相关的内容。通常一个网站性能好不好,我们能够在体验时主观地感受到,比如打开时很慢、滚动时卡顿,或者点击按钮后很久才响应等等。但如果要准确地判断到网页的性能到底如何,是需要依赖具体指标的。

下面介绍三个常用的指标,分别是:FCP(首次内容绘制)、LCP(最大内容绘制)以及 CLS(积累布局偏移)。

FCP(首次内容绘制)

FCP 是一个关键指标,用来测量页面从开始加载到第一个页面内容在屏幕上完成渲染的时间。越快的 FCP 时间能够让用户感知到网页有在正常运行,而不是停滞、无响应。

这里提到的 “内容” ,指的是文本、图像(包括背景图像)、<svg>元素或非白色的<canvas>元素。如下图所示,FCP 出现在第二帧。

LCP(最大内容绘制)

LCP 指的从页面开始加载到可视区域内可见的「最大图像」或「文本块」完成渲染的时间。

这里提到的「最大图像」或「文本块」,具体来说是包含<img>元素、内嵌在<svg>元素内的<image>元素、块级文本等。

而测量方式,则是在页面加载过程中,记录视窗内的元素的渲染大小,取尺寸最大的元素,回溯这个元素被完整渲染的时间。注意,如果元素的有一部分在视窗外,在视窗外的部分不会参与到尺寸比较当中。

如下图所示,LCP 发生在第二帧,因为这个时候渲染尺寸最大的文本块被渲染出来了。后续帧当中,可能渲染出了一些图片,但尺寸都比文本块小,所以文本块依然是这个视窗内的最大元素。

CLS(积累布局偏移)

CLS 是指可视区域内发生的最大布局偏移分数。简单来说就是测量页面在加载时,元素的位置出现意外的偏移情况,如果元素尺寸大,而且位置偏移比较远,那么 CLS 分数就会显著增高。

这个指标会跟实际的用户操作或者体验有直接相关,所以应该也会是咱们设计师需要重点关注的内容,因为有时候布局偏移,是会比较影响用户获取信息、或者进行操作,甚至引发一些不可挽回的损失。

然后我来介绍一下测量网页性能的工具吧。我自己用过这两个,发现其实没啥差别,大家看喜好使用即可:

两个工具都能模拟桌面设备或者移动设备,记录多项关键指标的数据,并给出改进建议。

观察页面性能情况,不仅仅是前端技术人员要做的事情,了解到设计师也是可以参与到其中的。

比如 Guillaume Granger,他会比较想控制页面中 JavaScript 的数量,所以它提到,他会将所有用了 JavaScript 相关信息记录在表格当中。之后每次在网页中使用 JavaScript 时,都会跟之前的记录进行比对,判断重要性,决定是否在这个位置上使用 JavaScript。

开发者 Thomas Kelly 则提出了当意识到页面性能出现瓶颈时,需要做的事情,比如:

  • 制定一个目标,团队一起往这个目标前进;
  • 高频收集页面性能数据;
  • 尝试用不同方式来解决同一个问题;
  • 与同伴分享对性能的一些新发现…

资源分享

最后来分享一下相关的资源吧,包含两个插件、三个学习网站以及一个 React 课程。

插件:VisBug

介绍一个谷歌官方出品的插件:VisBug,主要用来帮助用户在浏览网页时进行调试和设计,包括编辑和可视化页面的 CSS,尺寸和字体等元素。

插件:Motion DevTools

Motion DevTools 是一个检查网页动效的插件,可视化和分析用户交互设计中的滚动、动画效果,并支持实时编辑、预览或导出等功能。

网站推荐

接下来介绍三个在国内外拥有较高知名度和影响力的设计师和开发人员。他们的观点、经验分享往往能给我带来一些新的启发。尤其是他们对钻研新技术的热情,是非常强烈的。

课程推荐

最后强烈推荐一门 React 课程——The Joy of React,这个课程我在年初的文章也有提到过,是以互动式课程的形式,由浅入深地讲解 React。从基础的组件 props 和 JSX 知识,到 Hooks、API 设计等等,讲述非常清晰,强烈推荐。

分享会感想

分享完之后感觉效果可能还不错,大家都有各自的收获。而且分享会中也不时有人提出相关问题,我也一一进行解答了。

或者也有对我分享内容的一些补充,比如我在分享完 Devtools 环节的时候,有同事也分享了一个在 Application — Cookie 面板里快速切换网页语言的方法。

后面了解到大家对于 CSS 和 React 那块听的比较迷糊,因为原本没有实践过的话,会对这些没有什么概念。而且大家好像对 🛝Playground 没有什么兴趣,并没有人对里面的内容有什么提问和看法之类的,可能到这一步都比较迷糊?🤔

指标那块倒是有不少同事关心,问了几个问题,比如有哪些方法来去改进几个指标的数据,或者在设计过程中是否可以提前避免性能问题等等。

总体来说和之前的分享会相比,这次分享会的参与度比较不错。

MIUI稳定版12.0直升12.5开发版方法

By: fengooge
27 March 2021 at 11:30
从网络反馈来看,最新的 MIUI 12.5 无论是在系统流畅性上还是隐私保护上,都有非常明显的提升,我在升级之后的第一感受就是系统真的很流畅,比我上一个 MIUI 12.0.7 稳定版有非常大的提升。MIUI 12.5 虽然是测试版系统,但是稳定性和完成度上都很高,推荐大家都升级。关于 MIUI 12.5 的官方介绍页面:https://home.miui.com/MIUI 测试版系统的常规升级方法,是需要去官方申请内测或公测名额。但是现在官方内测和公测通道都已经关闭,想借用别人已经有权限的小米账号,又有隐私泄露的风险。这里就介绍一个非常规方法,也被很多人戏称为替换安装包的「偷渡法」。【方法及步骤简介】1、下载好可以升级的 MIUI 12.5 内测版安装包(A包);这是小米官方 MIUI 安装包的 GitHub 下载页面:https://github.com/mooseIre/

❌
❌