概要
SwiftUIのTextなどは、Markdown記法が使えるようになりました。例えば以下のTextは、
struct MarkdownView: View {
var body: some View {
List {
Text("**BOLD**")
}
}
}
実際にプレビューに表示するとBold表示になります。
パラメータとしてマークダウン文字列を取得する
例えばQiita API v2などは、レスポンスの中にMarkdownで記述されたbodyがあります。これをMarkdownで表示しようと思います。
(Qiita APIからデータを取得するためのEnvironment実装はおまけで紹介します)
struct MarkdownView: View {
/// Qiita APIからデータを受け取るためのEnvironment
@Environment(\.qiita) private var qiita
@State private var markdown = ""
var body: some View {
List {
Text(markdown)
}
.onAppear {
let itemId = "c686397e4a0f4f11683d" // Markdown記法 チートシート記事ID
Task {
markdown = try await qiita.get("/api/v2/items/\(itemId)").body?.stringValue() ?? ""
}
}
}
}
しかしこれを実際に表示したところ、Qiitaから取得したマークダウンの表示はプレーンなテキスト表示になりました。
マークダウンとして文字列を認識させる
次のようにTextの中身の値に.init
を使うことで文字列がマークダウンとして認識されるようになります。
struct MarkdownView: View {
/// Qiita APIからデータを受け取るためのEnvironment
@Environment(\.qiita) private var qiita
@State private var markdown = ""
var body: some View {
List {
Text(.init(markdown)) // ← 変更点
}
.onAppear {
let itemId = "c686397e4a0f4f11683d" // Markdown記法 チートシート記事ID
Task {
markdown = try await qiita.get("/api/v2/items/\(itemId)").body?.stringValue() ?? ""
}
}
}
}
この.init
ですが、これは実際はLocalizedStringKey
のinitになります。
なので、Text(.init(markdown))
の部分をText(LocalizedStringKey(markdown))
に変更しても同様に動作します。
注意点
iOS 14ではプレーンテキストとして表示される
マークダウン記法対応は iOS15からなので、iOS14ではLocalizedStringKeyを用いたとしてもプレーンテキストで表示が行われます。
受け取った文字列がLocalizable.stringのキーと同一の場合、Localizable.stringの文字列が表示される
内部でLocalizedStringKeyを使っているので当然といえば当然なんですが、もし受け取った文字列がLocalizable.stringのキーと同一だった場合、Localizable.stringに定義された文字列が出力されてしまいます。
struct MarkdownView: View {
@State private var markdown = ""
var body: some View {
List {
Text(.init(markdown))
}
.onAppear {
Task {
markdown = "App description" //← Localizable.stringに定義されている
}
}
}
}
struct MarkdownView_Previews: PreviewProvider {
static var previews: some View {
MarkdownView()
.environment(\.locale, .init(identifier: "ja-JP"))
}
}
おまけ
今回のコード説明ではQiita APIからデータを取得するために、Qiita
のEnvironmentと、そのレスポンスJSON
を作成しています。このQiitaクラスはgetのみの対応になっています。
import Foundation
struct QiitaError: Error {
let json: JSON
}
struct Qiita {
internal init(host: String) {
self.get = Get(host: host)
}
private static let scheme = "https"
let get: Get
@dynamicCallable
struct Get {
let host: String
func dynamicallyCall(withKeywordArguments pairs: KeyValuePairs<String, String?>) async throws -> JSON {
var components = URLComponents()
components.scheme = Qiita.scheme
components.host = host
components.path = (pairs.filter { $0.key.isEmpty }.first?.value)!
components.queryItems = pairs.filter({ !$0.key.isEmpty}).map({
URLQueryItem(name: $0.key, value: $0.value)
})
let data = try await URLSession.shared.data(from: components.url!)
let response: HTTPURLResponse = data.1 as! HTTPURLResponse
let json = try JSON(data: data.0)
switch response.statusCode {
case 200: return json
default: throw QiitaError(json: json)
}
}
}
}
import SwiftUI
private struct QiitaKey: EnvironmentKey {
static let defaultValue: Qiita = .init(host: "qiita.com")
}
extension EnvironmentValues {
var qiita: Qiita {
get { self[QiitaKey.self] }
set { self[QiitaKey.self] = newValue }
}
}
extension View {
func qiita(_ qiita: Qiita) -> some View {
environment(\.qiita, qiita)
}
}
import Foundation
typealias NumberValue = Double // IEEE 754 specification (BinaryFloatingPoint)
@dynamicMemberLookup
enum JSON {
case dictionaryValue(Dictionary<String, JSON>)
case arrayValue(Array<JSON>)
case numberValue(NumberValue)
case stringValue(String)
case boolValue(Bool)
case nullValue
func objectValue() -> Dictionary<String, JSON>? {
if case .dictionaryValue(let dictionary) = self {
return dictionary
}
return nil
}
func arrayValue() -> Array<JSON>? {
if case .arrayValue(let array) = self {
return array
}
return nil
}
func stringValue() -> String? {
if case .stringValue(let str) = self {
return str
}
return nil
}
func numberValue() -> NumberValue? {
if case .numberValue(let number) = self {
return number
} else if case .boolValue(let b) = self {
return NumberValue(b)
}
return nil
}
func boolValue() -> Bool? {
if case .boolValue(let bool) = self {
return bool
}
return nil
}
func nullValue() -> NSNull? {
if case .nullValue = self {
return NSNull()
}
return nil
}
subscript(index: Int) -> JSON? {
if case .arrayValue(let array) = self {
return index < array.count ? array[index] : nil
}
return nil
}
subscript(dynamicMember member: String) -> JSON? {
if case .dictionaryValue(let dict) = self {
return dict[member]
}
return nil
}
private init(_ object: Any) {
switch object {
case let boolValue as Bool: self = .boolValue(boolValue)
case let numberValue as NumberValue: self = .numberValue(numberValue)
case let stringValue as String: self = .stringValue(stringValue)
case let dictionaryValue as Dictionary<String, Any>: self = JSON.dictionaryValue(dictionaryValue.mapValues{ JSON($0) })
case let arrayValue as Array<Any>: self = .arrayValue(arrayValue.map { JSON($0)} )
default: self = .nullValue
}
}
init(data: Data) throws {
let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
self = .init(jsonObject)
}
}
private extension ExpressibleByIntegerLiteral {
init(_ booleanLiteral: BooleanLiteralType) {
self = booleanLiteral ? 1 : 0
}
}