]
+ }
+
+ func testUpdateAccountWithEmptyItems() {
+ let testItems = support.makeParsedItemTestDataFor(numberOfFeeds: 0, numberOfItemsInFeeds: 0)
+ let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
+ let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItems)
+
+ let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
+
+ let completionExpectation = expectation(description: "Did Finish")
+ update.completionBlock = {
+ completionExpectation.fulfill()
+ }
+
+ OperationQueue.main.addOperation(update)
+
+ waitForExpectations(timeout: 2)
+
+ let entries = testItems.flatMap { $0.value }
+ let articleIds = Set(entries.compactMap { $0.syncServiceID })
+ XCTAssertEqual(articleIds.count, entries.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
+
+ let accountArticles = account.fetchArticles(.articleIDs(articleIds))
+ XCTAssertTrue(accountArticles.isEmpty)
+ }
+
+ func testUpdateAccountWithOneItem() {
+ let testItems = support.makeParsedItemTestDataFor(numberOfFeeds: 1, numberOfItemsInFeeds: 1)
+ let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
+ let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItems)
+
+ let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
+
+ let completionExpectation = expectation(description: "Did Finish")
+ update.completionBlock = {
+ completionExpectation.fulfill()
+ }
+
+ OperationQueue.main.addOperation(update)
+
+ waitForExpectations(timeout: 2)
+
+ let entries = testItems.flatMap { $0.value }
+ let articleIds = Set(entries.compactMap { $0.syncServiceID })
+ XCTAssertEqual(articleIds.count, entries.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
+
+ let accountArticles = account.fetchArticles(.articleIDs(articleIds))
+ XCTAssertTrue(accountArticles.count == entries.count)
+
+ let accountArticleIds = Set(accountArticles.map { $0.articleID })
+ let missingIds = articleIds.subtracting(accountArticleIds)
+ XCTAssertTrue(missingIds.isEmpty)
+ }
+
+ func testUpdateAccountWithManyItems() {
+ let testItems = support.makeParsedItemTestDataFor(numberOfFeeds: 100, numberOfItemsInFeeds: 100)
+ let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
+ let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItems)
+
+ let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
+
+ let completionExpectation = expectation(description: "Did Finish")
+ update.completionBlock = {
+ completionExpectation.fulfill()
+ }
+
+ OperationQueue.main.addOperation(update)
+
+ waitForExpectations(timeout: 10) // 10,000 articles takes ~ three seconds for me.
+
+ let entries = testItems.flatMap { $0.value }
+ let articleIds = Set(entries.compactMap { $0.syncServiceID })
+ XCTAssertEqual(articleIds.count, entries.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
+
+ let accountArticles = account.fetchArticles(.articleIDs(articleIds))
+ XCTAssertTrue(accountArticles.count == entries.count)
+
+ let accountArticleIds = Set(accountArticles.map { $0.articleID })
+ let missingIds = articleIds.subtracting(accountArticleIds)
+ XCTAssertTrue(missingIds.isEmpty)
+ }
+
+ func testCancelUpdateAccount() {
+ let testItems = support.makeParsedItemTestDataFor(numberOfFeeds: 1, numberOfItemsInFeeds: 1)
+ let resource = FeedlyCategoryResourceId(id: "user/12345/category/6789")
+ let provider = TestItemsByFeedProvider(providerName: resource.id, parsedItemsKeyedByFeedId: testItems)
+
+ let update = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: provider, log: support.log)
+
+ let completionExpectation = expectation(description: "Did Finish")
+ update.completionBlock = {
+ completionExpectation.fulfill()
+ }
+
+ OperationQueue.main.addOperation(update)
+
+ update.cancel()
+
+ waitForExpectations(timeout: 2)
+
+ let entries = testItems.flatMap { $0.value }
+ let articleIds = Set(entries.compactMap { $0.syncServiceID })
+ XCTAssertEqual(articleIds.count, entries.count, "Not every item has a value for \(\ParsedItem.syncServiceID).")
+
+ let accountArticles = account.fetchArticles(.articleIDs(articleIds))
+ XCTAssertTrue(accountArticles.isEmpty)
+ }
+}
diff --git a/Frameworks/Account/AccountTests/Feedly/Initial/mustread_initial.json b/Frameworks/Account/AccountTests/Feedly/Initial/mustread_initial.json
deleted file mode 100644
index 467346439..000000000
--- a/Frameworks/Account/AccountTests/Feedly/Initial/mustread_initial.json
+++ /dev/null
@@ -1 +0,0 @@
-{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.must","items":[]}
\ No newline at end of file
diff --git a/Frameworks/Account/AccountTests/Feedly/Initial/programming_initial.json b/Frameworks/Account/AccountTests/Feedly/Initial/programming_initial.json
deleted file mode 100644
index f963dfd34..000000000
--- a/Frameworks/Account/AccountTests/Feedly/Initial/programming_initial.json
+++ /dev/null
@@ -1 +0,0 @@
-{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/885f2e01-d314-4e63-abac-17dcb063f5b5","updated":1569829987669,"continuation":"16d635b7080:1d8b:90d684ff","items":[{"keywords":["Apple","Mac","Xcode"],"originId":"https://indiestack.com/?p=858","fingerprint":"6a684f62","id":"FQCNcmB2/bN37jEqK6evDxyNDr+PE839IP+rONrBVrE=_16d8128a555:4da02:18991ffa","author":"Daniel Jalkut","summary":{"direction":"ltr","content":"With the release of macOS 10.15 fast-approaching, more and more Mac developers will be scurrying to ensure their apps are notarized. This is the process by which binary applications are submitted to Apple for cryptographic seal-of-approval indicating that the app meets minimum requirements for safety, and shows no obvious signs of being malware. Apple offers … Continue reading Notarization Provider IDs →"},"alternate":[{"href":"https://indiestack.com/2019/09/notarization-provider-ids/","type":"text/html"}],"crawled":1569829987669,"title":"Notarization Provider IDs","published":1568560761000,"origin":{"streamId":"feed/http://indiestack.com/feed/","htmlUrl":"https://indiestack.com","title":"Indie Stack"},"content":{"direction":"ltr","content":"With the release of macOS 10.15 fast-approaching, more and more Mac developers will be scurrying to ensure their apps are notarized. This is the process by which binary applications are submitted to Apple for cryptographic seal-of-approval indicating that the app meets minimum requirements for safety, and shows no obvious signs of being malware.
\nApple offers substantial documentation about notarizing your apps. Many developers will find that Xcode automatically notarizes the app as part of the built-in process for archiving an app for release. For those of us with existing, automated command-line build & release processes, there is a separate guide just for us:
\n\nCustomizing the Notarization Workflow
\nThe steps for automating notarization involve running the “altool” command from Terminal. Everything in the guide linked above should work perfectly unless you’re a member of more than one development team. If you have more than one team associated with your Apple ID, the back-end at Apple doesn’t know which one it should notarize on behalf of. You’ll see an error message like this:
\n\nError: Your Apple ID account is attached to other iTunes providers. You will need to specify which provider you intend to submit content to by using the -itc_provider command. Please contact us if you have questions or need help. (1627)
\nHere’s where things get fun: what the heck is your ITC provider ID? It’s not listed anywhere obvious on the Apple developer site or in Xcode, and can’t be obtained from the very tool that is asking for it. I came across a message from the ever-helpful Quinn in the Apple Developer Forums. It details a method for locating the provider ID by running a command-line tool, iTMSTransporter, from deep within Apple’s Application Loader app.
\nApplication Loader has since been eliminated from Xcode 11, so if you’re running with modern tools, you’ll be hard pressed to find it. Fear not, the binary is preserved deep within the Xcode app bundle itself:
\n\n% xcrun -f iTMSTransporter\n/Users/daniel/Applications/Xcode/Xcode.app/Contents/Developer/usr/bin/iTMSTransporter\n
\nAll that said, here is a surefire list of steps for obtaining your ITC Provider ID, or as it’s described in the altool man page, your ASC Provider Shortcode.
\n\n- Create a new App-Specific Password from your Apple ID management page.
\n- From Terminal, invoke iTMSTransporter with the following options:\n
\nxcrun iTMSTransporter -m provider -u <yourAppleID> -p <yourAppSpecificPassword>\n
\n \n- At your discretion, revoke the App-Specific Password you created for this process.
\n
\nNOTE: These instructions apply if you are using Xcode 11. If you’re still using Xcode 10, you’ll need to dig up the iTMSTransporter binary from within Application Loader.app. Instead of “xcrun iTMSTransporter” above, it will be something like /path/to/Application Loader.app/Contents/itms/bin/iTMSTransporter.
\nIf all goes well, you should see a list of your Apple development teams, including the Long Name and Short Name. The Short Name is what you need to pass whenever altool requires an ITC or ASC Provider ID.
"},"visual":{"url":"http://b.vimeocdn.com/ts/442/609/442609877_1280.jpg","width":1280,"height":720,"contentType":"image/jpeg"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/885f2e01-d314-4e63-abac-17dcb063f5b5","label":"Programming"}]},{"keywords":["Blog"],"originId":"https://www.weheartswift.com/?p=141167","fingerprint":"ddc1f0fc","id":"l+udnvvnAeQSl5COMMeF43kFMnjb8UK43/2J+Rysa8Q=_16d8127c266:4d99c:18991ffa","author":"Florian Marcu","summary":{"direction":"ltr","content":"It’s been 6 years since Apple launched the first version of Swift programming language. During these years, both the language itself and the ecosystem around it have evolved tremendously. And so did the online resources and options on how to learn Swift. In this article, we are taking a look at a different approach to learning Swift, which is often…
\nRead more
\nThe post How to Learn Swift Programming Practically appeared first on We ❤ Swift.
"},"alternate":[{"href":"https://www.weheartswift.com/how-to-learn-swift-programming/","type":"text/html"}],"crawled":1569829929574,"title":"How to Learn Swift Programming Practically","published":1568276656000,"origin":{"streamId":"feed/http://www.weheartswift.com/feed/","htmlUrl":"https://www.weheartswift.com","title":"We ❤ Swift"},"visual":{"url":"https://cdn.vox-cdn.com/thumbor/fhKs50EFS0SJPjSjfCR7lXwcMfs=/0x0:2040x1360/1310x873/cdn.vox-cdn.com/uploads/chorus_image/image/59667903/acastro_180508_1777_google_IO_0002.0.jpg","width":1310,"height":873,"contentType":"image/jpeg"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/885f2e01-d314-4e63-abac-17dcb063f5b5","label":"Programming"}]},{"id":"Yn8gu9QqU/Nu8FbPliaB+X/354R2g5xNVMORFC3HITI=_16d8126f6ee:1c4e:90d684ff","keywords":["Content, Content Strategy"],"originId":"https://alistapart.com/article/the-untapped-power-of-vulnerability-transparency-in-content-strategy/","fingerprint":"173b0f1c","title":"The Untapped Power of Vulnerability & Transparency in Content Strategy","author":"by Travis McKnight","summary":{"content":"\nIn marketing, transparency and vulnerability are unjustly stigmatized. The words conjure illusions of being frightened, imperfect, and powerless. And for companies that shove carefully curated personas in front of users, little is more terrifying than losing control of how people perceive the brand.
\n\n\nLet’s shatter this illusioned stigma. Authentic vulnerability and transparency are strengths masquerading as weaknesses. And companies too scared to embrace both traits in their content forfeit bona fide user-brand connections for often shallow, misleading engagement tactics that create fleeting relationships.
\n\n\nTransparency and vulnerability are closely entwined concepts, but each one engages users in a unique way. Transparency is how much information you share, while vulnerability is the truth and meaning behind your actions and words. Combining these ideas is the trick to creating empowering and meaningful content. You can’t tell true stories of vulnerability without transparency, and to be authentically transparent you must be vulnerable.
\n\n\nTo be vulnerable, your brand and its content must be brave, genuine, humble, and open, all of which are traits that promote long-term customer loyalty. And if you’re transparent with users about who you are and about your business practices, you’re courting 94 percent of consumers who say they’re more loyal to brands that offer complete openness and 89 percent of people who say they give transparent companies a second chance after a bad experience.
\n\n\nFor many companies, being completely honest and open with their customers—or employees, in some cases—only happens in a crisis. Unfortunately for those businesses, using vulnerability and transparency only as a crisis management strategy diminishes how sincere they appear and can reduce customer satisfaction.
\n\n\nUnlocking the potential of being transparent and vulnerable with users isn’t a one-off tactic or quick-fix emergency response tool—it’s a commitment to intimate storytelling that embraces a user’s emotional and psychological needs, which builds a meaningful connection between the storyteller and the audience.
\n\n\nThe three storytelling pillars of vulnerable and transparent content
\n\n\nIn her book, Braving the Wilderness, sociologist Brené Brown explains that vulnerability connects us at an emotional level. She says that when we recognize someone is being vulnerable, we invest in their story and begin to develop an emotional bond. This interwoven connection encourages us to experience the storyteller’s joy and pain, and then creates a sense of community and common purpose among the person being vulnerable and the people who acknowledge that vulnerability.
\n\n\nThree pillars in a company’s lifecycle embrace this bond and provide an outline for telling stories worthy of a user’s emotional investment. The pillars are:
\n\n\n- the origins of a company, product, idea, or situation;
- intimate narratives about customers’ life experiences;
- and insights about product success and failure.
\n\n\nOrigin stories
\n\n\nAn origin story spins a transparent tale about how a company, product, service, or idea is created. It is often told by a founder, CEO, or industry innovator. This pillar is usually used as an authentic way to provide crisis management or as a method to change how users feel about a topic, product, or your brand.
\n\n\nCustomers’ life experiences
\n\n\nWhile vulnerable origin stories do an excellent job of making users trust your brand, telling a customer’s personal life story is arguably the most effective way to use vulnerability to entwine a brand with someone’s personal identity.
\n\n\nUnlike an origin story, the customer experiences pillar is focused on being transparent about who your customers are, what they’ve experienced, and how those journeys align with values that matter to your brand. Through this lens, you’ll empower your customers to tell emotional, meaningful stories that make users feel vulnerable in a positive way. In this situation, your brand is often a storytelling platform where users share their story with the brand and fellow customers.
\n\n\nProduct and service insights
\n\n\nOrigin stories make your brand trustworthy in a crisis, and customers’ personal stories help users feel an intimate connection with your brand’s persona and mission. The last pillar, product and service insights, combines the psychological principles that make origin and customer stories successful. The outcome is a vulnerable narrative that rallies users’ excitement about, and emotional investment in, what a company sells or the goals it hopes to achieve.
\n\n\nVulnerability, transparency, and the customer journey
\n\n\nThe three storytelling pillars are crucial to embracing transparency and vulnerability in your content strategy because they let you target users at specific points in their journey. By embedding the pillars in each stage of the customer’s journey, you teach users about who you are, what matters to you, and why they should care.
\n\n\nFor our purposes, let’s define the user journey as:
\n\n\n- awareness;
- interest;
- consideration;
- conversion;
- and retention.
\n\n\nAwareness
\n\n\nPeople give each other seven seconds to make a good first impression. We’re not so generous with brands and websites. After discovering your content, users determine if it’s trustworthy within one-tenth of a second.
\n\n\nPage design and aesthetics are often the determining factors in these split-second choices, but the information users discover after that decision shapes their long-term opinions about your brand. This snap judgement is why transparency and vulnerability are crucial within awareness content.
\n\n\nWhen you only get one chance to make a positive first impression with your audience, what content is going to be more memorable?
\n\n\nTypical marketing “fluff” about how your brand was built on a shared vision and commitment to unyielding customer satisfaction and quality products? Or an upfront, authentic, and honest story about the trials and tribulations you went through to get where you are now?
\n\n\nBuffer, a social media management company that helped pioneer the radical transparency movement, chose the latter option. The outcome created awareness content that leaves a positive lasting impression of the brand.
\n\n\nIn 2016, Joel Gascoigne, cofounder and CEO of Buffer, used an origin story to discuss the mistakes he and his company made that resulted in laying off 10 employees.
\n\n\nIn the blog post “Tough News: We’ve Made 10 Layoffs. How We Got Here, the Financial Details and How We’re Moving Forward,” Gascoigne wrote about Buffer’s over-aggressive growth choices, lack of accountability, misplaced trust in its financial model, explicit risk appetite, and overenthusiastic hiring. He also discussed what he learned from the experience, the changes Buffer made based on these lessons, the consequences of those changes, and next steps for the brand.
\n\n\nGascoigne writes about each subject with radical honesty and authenticity. Throughout the article, he’s personable and relatable; his tone and voice make it obvious he’s more concerned about the lives he’s irrevocably affected than the public image of his company floundering. Because Gascoigne is so transparent and vulnerable in the blog post, it’s easy to become invested in the narrative he’s telling. The result is an article that feels more like a deep, meaningful conversation over coffee instead of a carefully curated, PR-approved response.
\n\n\nYes, Buffer used this origin story to confront a PR crisis, but they did so in a way that encouraged users to trust the brand. Buffer chose to show up and be seen when they had no control over the outcome. And because Gascoigne used vulnerability and transparency to share the company’s collective pain, the company reaped positive press coverage and support on social media—further improving brand awareness, user engagement, and customer loyalty.
\n\n\nHowever, awareness content isn’t always brand focused. Sometimes, smart awareness content uses storytelling to teach users and shape their worldviews. The 2019 State of Science Index is an excellent example.
\n\n\nThe annual State of Science Index evaluates how the global public perceives science. The 2019 report shows that 87 percent of people acknowledge that science is necessary to solve the world’s problems, but 33 percent are skeptical of science and believe that scientists cause as many problems as they solve. Furthermore, 57 percent of respondents are skeptical of science because of scientists’ conflicting opinions about topics they don’t understand.
\n\n\n3M, the multinational science conglomerate that publishes the report, says the solution for this anti-science mindset is to promote intimate storytelling among scientists and layfolk.
\n\n\n3M creates an origin story with its awareness content by focusing on the ins and outs of scientific research. The company is open and straightforward with its data and intentions, eliminating any second guesses users might have about the content they’re digesting.
\n\n\nThe company kicked off this strategy on three fronts, and each storytelling medium interweaves the benefits of vulnerability and transparency by encouraging researchers to tell stories that lead with how their findings benefit humanity. Every story 3M tells focuses on breaking through barriers the average person faces when they encounter science and encouraging scientists to be vulnerable and authentic with how they share their research.
\n\n\nFirst, 3M began a podcast series known as Science Champions. In the podcast, 3M Chief Science Advocate Jayshree Seth interviews scientists and educators about the global perception of science and how science and scientists affect our lives. The show is currently in its second season and discusses a range of topics in science, technology, engineering, and math.
\n\n\nSecond, the company worked with science educators, journalist Katie Couric, actor Alan Alda, and former NASA astronaut Scott Kelly to develop the free Scientists as Storytellers Guide. The ebook helps STEM researchers improve how and why they communicate their work with other people—with a special emphasis on being empathetic with non-scientists. The guide breaks down how to develop communications skills, overcome common storytelling challenges, and learn to make science more accessible, understandable, and engaging for others.
\n\n\nLast, 3M created a film series called Beyond the Beaker that explores the day-to-day lives of 3M scientists. In the short videos, scientists give the viewer a glimpse into their hobbies and home life. The series showcases how scientists have diverse backgrounds, hobbies, goals, and dreams.
\n\n\nUnlike Buffer, which benefits directly from its awareness content, 3M’s three content mediums are designed to create a long-term strategy that changes how people understand and perceive science, by spreading awareness through third parties. It’s too early to conclude that the strategy will be successful, but it’s off to a good start. Science Champions often tops “best of” podcast lists for science lovers, and the Scientists as Storytellers Guide is a popular resource among public universities.
\n\n\nInterest
\n\n\nHow do you court new users when word-of-mouth and organic search dominate how people discover new brands? Target their interests.
\n\n\nNow, you can be like the hundreds of other brands that create a “10 best things” list and hope people stumble onto your content organically and like what they see. Or, you can use content to engage with people who are passionate about your industry and have genuine, open discussions about the topics that matter to you both.
\n\n\nThe latter option is a perfect fit for the product and service insights pillar, and the customers’ life experiences pillar.
\n\n\nTo succeed in these pillars you must balance discussing the users’ passions and how your brand plays into that topic against appearing disingenuous or becoming too self-promotional.
\n\n\nNonprofits have an easier time walking this taut line because people are less judgemental when engaging with NGOs, but it’s rare for a for-profit company to achieve this balance. SpaceX and Thinx are among the few brands that are able to walk this tightrope.
\n\n\nThinx, a women’s clothing brand that sells period-proof underwear, uses its blog to generate awareness, interest, and consideration content via the customers’ life experiences pillar. The blog, aptly named Periodical, relies on transparency and vulnerability as a cornerstone to engage users about reproductive and mental health.
\n\n\nToni Brannagan, Thinx’s content editor, says the brand embraces transparency and vulnerability by sharing diverse ideas and personal experiences from customers and experts alike, not shying away from sensitive subjects and never misleading users about Thinx or the subjects Periodical discusses.
\n\n\nAs a company focused on women’s healthcare, the product Thinx sells is political by nature and entangles the brand with themes of shame, cultural differences, and personal empowerment. Thinx’s strategy is to tackle these subjects head-on by having vulnerable conversations in its branding, social media ads, and Periodical content.
\n\n\n“Vulnerability and transparency play a role because you can’t share authentic diverse ideas and experiences about those things—shame, cultural differences, and empowerment—without it,” Brannagan says.
\n\n\nA significant portion of Thinx’s website traffic is organic, which means Periodical’s interest-driven content may be a user’s first touchpoint with the brand.
\n\n\n“We’ve seen that our most successful organic content is educational, well-researched articles, and also product-focused blogs that answer the questions about our underwear, in a way that’s a little more casual than what’s on our product pages,” Brannagan says. “In contrast, our personal essays and ‘more opinionated’ content performs better on social media and email.”
\n\n\nThanks in part to the blog’s authenticity and open discussions about hard-hitting topics, readers who find the brand through organic search drive the most direct conversions.
\n\n\nConversations with users interested in the industry or topic your company is involved in don’t always have to come from the company itself. Sometimes a single person can drive authentic, open conversations and create endearing user loyalty and engagement.
\n\n\nFor a company that relies on venture capital investments, NASA funding, and public opinion for its financial future, crossing the line between being too self-promotional and isolating users could spell doom. But SpaceX has never shied away from difficult or vulnerable conversations. Instead, the company’s founder, Elon Musk, embraces engaging with users interests in public forums like Twitter and press conferences.
\n\n\n
\n\n\nMusk’s tweets about SpaceX are unwaveringly authentic and transparent. He often tweets about his thoughts, concerns, and the challenges his companies face. Plus, Musk frequently engages with his Twitter followers and provides candid answers to questions many CEOs avoid discussing. This authenticity has earned him a cult-like following.
\n\n\n
\n\n\nMusk and SpaceX create conversations that target people’s interests and use vulnerability to equally embrace failure and success. Both the company and its founder give the public and investors an unflinching story of space exploration.
\n\n\nAnd despite laying off 10 percent of its workforce in January of 2019, SpaceX is flourishing. In May 2019, its valuation had risen to $33.3 billion and reported annual revenue exceeded $2 billion. It also earned global media coverage from launching Musk’s Tesla Roadster into space, recently completed a test flight of its Crew Dragon space vehicle, and cemented multiple new payload contracts.
\n\n\nBy engaging with users on social media and through standard storytelling mediums, Thinx and SpaceX bolster customer loyalty and brand engagement.
\n\n\nConsideration
\n\n\nModern consumers argue that ignorance is not bliss. When users are considering converting with a brand, 86 percent of consumers say transparency is a deciding factor. Transparency remains crucial even after they convert, with 85 percent of users saying they’ll support a transparent brand during a PR crisis.
\n\n\nYour brand must be open, clear, and honest with users; there is no longer another viable option.
\n\n\nSo how do you remain transparent while trying to sell someone a product? One solution employed by REI and Everlane is to be openly accountable to your brand and your users via the origin stories and product insights pillars.
\n\n\nREI, a national outdoor equipment retailer, created a stewardship program that behaves as a multifaceted origin story. The program’s content highlights the company’s history and manufacturing policies, and it lets users dive into the nitty-gritty details about its factories, partnerships, product production methods, manufacturing ethics, and carbon footprint.
\n\n\n
\n\n\nREI also employs a classic content hub strategy to let customers find the program and explore its relevant information. From a single landing page, users can easily find the program through the website’s global navigation and then navigate to every tangential topic the program encompasses.
\n\n\nREI also publishes an annual stewardship report, where users can learn intimate details about how the company makes and spends its money.
\n\n\n
\n\n\nEverlane, a clothing company, is equally transparent about its supply chain. The company promotes an insider’s look into its global factories via product insights stories. These glimpses tell the personal narratives of factory employees and owners, and provide insights into the products manufactured and the materials used. Everlane also published details of how they comply with the California Transparency in Supply Chains Act to guarantee ethical working conditions throughout its supply chain, including refusing to partner with human traffickers.
\n\n\n
\n\n\nThe crucial quality that Everlane and REI share is they publicize their transparency and encourage users to explore the shared information. On each website, users can easily find information about the company’s transparency endeavors via the global navigation, social media campaigns, and product pages.
\n\n\nThe consumer response to transparent brands like REI and Everlane is overwhelmingly positive. Customers are willing to pay price premiums for the additional transparency, which gives them comfort by knowing they’re purchasing ethical products.
\n\n\nREI’s ownership model has further propelled the success of its transparency by using it to create unwavering customer engagement and loyalty. As a co-op where customers can “own” part of the company for a one-time $20 membership fee, REI is beholden to its members, many of which pay close attention to its supply chain and the brands REI partners with.
\n\n\nAfter a deadly school shooting in Parkland, Florida, REI members urged the company to refuse to carry CamelBak products because the brand’s parent company manufactures assault-style weapons. Members argued the partnership violated REI’s supply chain ethics. REI listened and halted orders with CamelBak. Members rejoiced and REI earned a significant amount of positive press coverage.
\n\n\nConversion
\n\n\nImagine you’ve started incorporating transparency throughout your company, and promote the results to users. Your brand also begins engaging users by telling vulnerable, meaningful stories via the three pillars. You’re seeing great engagement metrics and customer feedback from these efforts, but not much else. So, how do you get your newly invested users to convert?
\n\n\nProvide users with a full-circle experience.
\n\n\nIf you combine the three storytelling pillars with blatant transparency and actively promote your efforts, users often transition from the consideration stage into the conversion state. Best of all, when users convert with a company that already earned their trust on an emotional level, they’re more likely to remain loyal to the brand and emotionally invested in its future.
\n\n\nThe crucial step in combining the three pillars is consistency. Your brand’s stories must always be authentic and your content must always be transparent. The outdoor clothing brand Patagonia is among the most popular and successful companies to maintain this consistency and excel with this strategy.
\n\n\nPatagonia is arguably the most vocal and aggressive clothing retailer when it comes to environmental stewardship and ethical manufacturing.
\n\n\nIn some cases, the company tells users not to buy its clothing because rampant consumerism harms the environment too much, which they care about more than profits. This level of radical transparency and vulnerability skyrocketed the company’s popularity among environmentally-conscious consumers.
\n\n\nIn 2011, Patagonia took out a full-page Black Friday ad in the New York Times with the headline “Don’t Buy This Jacket.” In the ad, Patagonia talks about the environmental toll manufacturing clothes requires.
\n\n\n“Consider the R2 Jacket shown, one of our best sellers. To make it required 135 liters of water, enough to meet the daily needs (three glasses a day) of 45 people. Its journey from its origin as 60 percent recycled polyester to our Reno warehouse generated nearly 20 pounds of carbon dioxide, 24 times the weight of the finished product. This jacket left behind, on its way to Reno, two-thirds [of] its weight in waste.”
\n\n\nThe ad encourages users to not buy any new Patagonia clothing if their old, ratty clothes can be repaired. To help, Patagonia launched a supplementary subdomain to its e-commerce website to support its Common Thread Initiative, which eventually got rebranded as the Worn Wear program.
\n\n\nPatatgonia’s Worn Wear subdomain gets users to engage with the company about causes each party cares about. Through Worn Wear, Patagonia will repair your old gear for free. If you’d rather have new gear, you can instead sell the worn out clothing to Patagonia, and they’ll repair it and then resell the product at a discount. This interaction encourages loyalty and repeat brand-user engagement.
\n\n\nIn addition, the navigation on Patagonia’s main website practically begs users to learn about the brand’s non-profit initiatives and its commitment to ethical manufacturing.
\n\n\n
\n\n\nToday, Patagonia is among the most respected, profitable, and trusted consumer brands in the United States.
\n\n\nRetention
\n\n\nContent strategy expands through nearly every aspect of the marketing stack, including ad campaigns, which take a more controlled approach to vulnerability and transparency. To target users in the retention stage and keep them invested in your brand, your goal is to create content using the customers’ life experiences pillar to amplify the emotional bond and brand loyalty that vulnerability creates.
\n\n\nAlways took this approach and ended up with one of its most successful social media campaigns.
\n\n\n
\n\n\nIn June 2014, Always launched its #LikeAGirl campaign to empower adolescent and teenage girls by transforming the phrase “like a girl” from a slur into a meaningful and positive statement.
\n\n\nThe campaign is centered on a video in which Always tasked children, teenagers, and adults to behave “like a girl” by running, punching, and throwing while mimicking their perception of how a girl performs the activity. Young girls performed the tasks wholeheartedly and with gusto, while boys and adults performed overly feminine and vain characterizations. The director then challenged the person on their portrayal, breaking down what doing things “like a girl” truly means. The video ends with a powerful, heart-swelling statement:
\n\n\n“If somebody else says that running like a girl, or kicking like a girl, or shooting like a girl is something you shouldn’t be doing, that’s their problem. Because if you’re still scoring, and you’re still getting to the ball in time, and you’re still being first...you’re doing it right. It doesn’t matter what they say.”
\n\n\nThis customer story campaign put the vulnerability girls feel during puberty front and center so the topic would resonate with users and give the brand a powerful, relevant, and purposeful role in this connection, according to an Institute for Public Relations campaign analysis.
\n\n\nConsequently, the #LikeAGirl campaign was a rousing success and blew past the KPIs Always established. Initially, Always determined an “impactful launch” for the video meant 2 million video views and 250 million media impressions, the analysis states.
\n\n\nFive years later, the campaign video has more than 66.9 million views and 42,700 comments on YouTube, with more than 85 percent of users reacting positively. Here are a few additional highlights the analysis document points out:
\n\n\n- Eighty-one percent of women ages 16–24 support Always in creating a movement to reclaim “like a girl” as a positive and inspiring statement.
- More than 1 million people shared the video.
- Thirteen percent of users created user-generated content about the campaign.
- The #LikeAGirl program achieved 4.5 billion global impressions.
- The campaign received 290 million social impressions, with 133,000 social mentions, and it caused a 195.3 percent increase in the brand’s Twitter followers.
\n\n\nAmong the reasons the #LikeAGirl content was so successful is that it aligned with Brené Brown’s concept that experiencing vulnerability creates a connection centered on powerful, shared emotions. Always then amplified the campaign’s effectiveness by using those emotions to encourage specific user behavior on social media.
\n\n\nHow do you know if you’re making vulnerable content?
\n\n\nDesigning a vulnerability-focused content strategy campaign begins by determining what kind of story you want to tell, why you want to tell it, why that story matters, and how that story helps you or your users achieve a goal.
\n\n\nWhen you’re brainstorming topics, the most important factor is that you need to care about the stories you’re telling. These tales need to be meaningful because if you’re weaving a narrative that isn’t important to you, it shows. And ultimately, why do you expect your users to care about a subject if you don’t?
\n\n\nLet’s say you’re developing a content campaign for a nonprofit, and you want to use your brand’s emotional identity to connect with users. You have a handful of possible narratives but you’re not sure which one will best unlock the benefits of vulnerability. In a Medium post about telling vulnerable stories, Cayla Vidmar presents a list of seven self-reflective questions that can reveal what narrative to choose and why.
\n\n\nIf you can answer each of Vidmar’s questions, you’re on your way to creating a great story that can connect with users on a level unrivaled by other methods. Here’s what you should ask yourself:
\n\n\n- What meaning is there in my story?
- Can my story help others?
- How can it help others?
- Am I willing to struggle and be vulnerable in that struggle (even with strangers)?
- How has my story shaped my worldview (what has it made me believe)?
- What good have I learned from my story?
- If other people discovered this good from their story, would it change their lives?
\n\n\nWhile you’re creating narratives within the three pillars, refer back to Vidmar’s list to maintain the proper balance between vulnerability and transparency.
\n\n\nWhat’s next?
\n\n\nYou now know that vulnerability and transparency are an endless fountain of strength, not a weakness. Vulnerable content won’t make you or your brand look weak. Your customers won’t flee at the sight of imperfection. Being human and treating your users like humans isn’t a liability.
\n\n\nIt’s time for your brand to embrace its untapped potential. Choose to be vulnerable, have the courage to tell meaningful stories about what matters most to your company and your customers, and overcome the fear of controlling how users will react to your content.
\n\n\nOrigin story
\n\n\nEvery origin story has six chapters:
\n\n\n- the discovery of a problem or opportunity;
- what caused this problem or opportunity;
- the consequences of this discovery;
- the solution to these consequences;
- lessons learned during the process;
- and next steps.
\n\n\nCustomers’ life experiences
\n\n\nEvery customer journey narrative has six chapters:
\n\n\n- plot background to frame the customer’s experiences;
- the customer’s journey;
- how the brand plays into that journey (if applicable);
- how the customer’s experiences changed them;
- what the customer learned from this journey;
- and how other people can use this information to improve their lives.
\n\n\nProduct and service insights
\n\n\nNarratives about product and service insights have seven chapters:
\n\n\n- an overview of the product/service;
- how that product/service affects users;
- why the product/service is important to the brand’s mission or to users;
- what about this product/service failed or succeeded;
- why did that success or failure happen;
- what lessons did this scenario create;
- and how are the brand and its users moving forward.
\n\n\nYou have the tools and knowledge necessary to be transparent, create vulnerable content, and succeed. And we need to tell vulnerable stories because sharing our experiences and embracing our common connections matters. So go ahead, put yourself out into the open, and see how your customers respond.
\n
","direction":"ltr"},"alternate":[{"href":"http://feedproxy.google.com/~r/alistapart/main/~3/MMGA19A7E-c/","type":"text/html"}],"canonical":[{"href":"https://alistapart.com/article/the-untapped-power-of-vulnerability-transparency-in-content-strategy/","type":"text/html"}],"crawled":1569829877486,"published":1569829877486,"origin":{"streamId":"feed/http://feeds.feedburner.com/alistapart/main","title":"A List Apart: The Full Feed","htmlUrl":"https://alistapart.com"},"visual":{"url":"http://www.blogcdn.com/www.engadget.com/media/2013/10/nvidia-shield-console-mode.jpg","width":620,"height":340,"contentType":"image/jpg"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/885f2e01-d314-4e63-abac-17dcb063f5b5","label":"Programming"}]},{"id":"Yn8gu9QqU/Nu8FbPliaB+X/354R2g5xNVMORFC3HITI=_16d8125222e:1c4d:90d684ff","keywords":["Career"],"originId":"https://alistapart.com/article/the-career-management-document/","fingerprint":"687902d8","title":"An Essential Tool for Capturing Your Career Accomplishments","author":"by Jessica Ivins","summary":{"content":"\nImagine you’re ready to apply for your next job. Like most busy professionals, you probably haven’t updated your résumé or your portfolio since you looked for your current job.
\n\n\nNow you need to update both, and you can’t remember what work you’ve done over the past few years. (In fact, you can barely remember what you’ve done over the past few months!)
\n\n\nSo you scramble to update your résumé with new content. Then you spend all weekend scraping together a new portfolio using screenshots of whatever work evidence you can find on your laptop. You submit the résumé and portfolio with your application, hoping you didn’t forget to include any major career milestones you achieved over the last few years.
\n\n\nThis is the process most of us use to approach our job search. We wait until we’re ready to find a job, panic at our lack of résumé and portfolio, and pull together a “good enough” version of each for the job application. (Trust me, I’ve done this many times myself.)
\n\n\nThis is a stressful and ineffective way to approach a job search. There’s a much better approach you can take—and you can start working on it now, even if you’re not on the job market.
\n\n\nThe Career Management Document
\n\n\nA Career Management Document (CMD) is a comprehensive collection of your résumé and portfolio content. It’s a document you update regularly, over time, with all the work you’ve done.
\n\n\nWhen you’re ready to apply for your next job, you’ll have all the résumé and portfolio pieces available in your CMD. All you need to do is assemble those pieces into résumé and portfolio documents, then send the documents off with your job application.
\n\n\nI update my CMD about once a week. I start by reviewing evidence of my recent work. I review Slack messages, Basecamp posts, emails, and any other current work-related content. I write my accomplishments in the format of résumé bullets, using the framework of responsibilities and accomplishments from this Manager Tools podcast. Then I add those bullets to the CMD.
\n\n\nHere are some examples from my CMD:
\n\n\n- Coached a student on writing a stronger portfolio story to showcase their advanced UX skills, resulting in the student getting a job interview.
- Facilitated an end-of-study analysis in under 90 minutes to help the team synthesize user research data from 12 participants.
- Led a remote retrospective with teams in two offices, developed actionable takeaways, and ended on time despite a delayed start.
\n\n\nMy CMD has several hundred résumé bullets, and it continues to grow. I organize content by year and by project. Within each project are responsibilities and accomplishments.
\n\n\nI add any content to the CMD that might go into my résumé someday. I include everything I can think of, even if it seems insignificant or trivial at the time.
\n\n\nFor example, I sometimes help with social media marketing at Center Centre, the UX design school where I’m a faculty member. I include it in my CMD. I don’t plan to pursue social media marketing as a career, but it may be relevant to a future job. Who knows—I may apply to work for an organization that makes social media marketing software someday. In that case, my social media experience could be relevant.
\n\n\nInclude portfolio artifacts with your CMD
\n\n\nIn addition to capturing bullets for my résumé, I capture content for my portfolio. Each week, I gather screenshots of my work, photos of me working with the team, and any other artifacts I can find. I store them in an organized system I can reference later.
\n\n\nI also take brief notes about the work I did and store them with the artifacts. That way, if I look back at these materials a year from now, I’ll have notes about what I did during the project, reminding me of the details.
\n\n\nFor example, after I facilitated a user research analysis session late last year, I captured evidence of it for my portfolio. I included photos of the whiteboard where I recorded public notes during the session. I also captured brief notes about who attended the session, the date, and when it took place during the project.
\n\n\nYou can use whatever tools you’d like to gather evidence of your work. I use Google Docs for the résumé portion of my CMD. I use Dropbox to store my portfolio artifacts. I create Dropbox folders with dates and project names that correspond to the contents of my CMD.
\n\n\n
Résumé content from my CMD. I wrote about coaching a student on crafting a presentation for her job interview. The highlighted areas are where I left comments reminding me of the details of the work. Note that some of the résumé bullets seem redundant, which is OK. When I create my next résumé, I’ll choose the most appropriate bullets.\n\n\n
I took notes on a whiteboard while coaching the student. I stored a photo of the whiteboard in Dropbox in a folder named with the date of the work and a description of what I did.\n\n\nThe key is to collect the evidence regularly and store it in an accessible, organized way that works for you. To know if you’re storing work evidence effectively, ask yourself, “Will I understand this CMD content a year from now based on how I’m capturing and storing it today?” If the answer is “yes,” you’re in good shape.
\n\n\nUpdate your CMD regularly
\n\n\nFor the CMD to work when you need it, it needs to be comprehensive and up-to-date. As I mentioned before, I update my CMD once a week. I schedule thirty minutes on my calendar each week so I remember to do it.
\n\n\nSometimes I have a busy week, and I can’t spend thirty minutes on my CMD. So I spend whatever amount of time I have. Some weeks, I only spend ten minutes. Ten minutes per week is better than zero minutes per week.
\n\n\nOccasionally, I don’t get a chance to update it because my week is so hectic. That’s OK because I’ll probably get to it the following week.
\n\n\nI recommend updating your CMD once a week and not once a month or once a quarter. If you wait even a month, you’ll have trouble remembering what you did three and a half weeks ago. Even worse, if you schedule a CMD update once a month and then miss it, you won’t get to it until the next month. That means you have to think back and remember two months of work, which is hard to do.
\n\n\nUpdating your CMD every week, while the work is fresh in your mind, gets the best results.
\n\n\nThe CMD benefits you in additional ways
\n\n\nThe CMD can help you prepare for your job search beyond your résumé and your portfolio.
\n\n\nYou can use it to prepare for a job interview. Since you’re capturing work evidence from each stage of the process in your CMD, you can use that evidence to remember what you did throughout a project. Then, you can craft a story about your role on that project.
\n\n\nHiring managers love to hear stories about your work during job interviews. For instance, if you’re a designer, they want to know the journey you took during your design process, from the start of a project to the end. A detailed CMD will help you remember this process so you can share it in an interview.
\n\n\nI’ve even used my CMD to write blog posts. I’ve been blogging regularly for the past two years, and I often refer to my CMD to remember work experience I had that’s relevant to what I’m writing. When I wrote the article “How to Tell Compelling Stories During a UX Job Interview,” I used my CMD to remember interview preparation exercises I did with students.
\n\n\nThe CMD can also help you track work accomplishments for your quarterly or annual performance reviews. Additionally, you can use it to write job ads when hiring for related roles on your team.
\n\n\nLastly, I find it rewarding to peruse my CMD now and then, especially when I look back at work I did over a year ago. The CMD serves as a record of all my professional accomplishments. This record helps me appreciate my professional growth because I see how far my skills have come over time.
\n\n\nLearn more about the CMD from Manager Tools
\n\n\nAt Center Centre, we originally learned about the Career Management Document through the Manager Tools podcast series.
\n\n\nManager Tools’ podcasts explain how to use a CMD for your résumé. We expanded their approach to include portfolio work as well. I recommend listening to their podcasts about creating and maintaining your CMD:
\n\n\n\n\n\nPrepare for your next job search now
\n\n\nWe tell our students at Center Centre that preparing for your next job search is a process that starts early. It’s like saving for retirement—the sooner you start saving money, the more likely you are to be prepared when the time comes.
\n\n\nSimilarly, collecting résumé and portfolio content ahead of time will prepare you to find your next job whenever you’re ready to do so. It also prepares you for a sudden job termination like an unexpected layoff. If you lose your job without warning, you’ll likely be under a lot of stress to find a new position. Having a CMD ready will relieve the additional stress of building a résumé and portfolio from scratch.
\n\n\nIf you don’t have a CMD yet, now is a great time to start one. Schedule 30 minutes this week to begin crafting your repository of work accomplishments. You’ll be glad you did when you seek your next job.
\n
","direction":"ltr"},"alternate":[{"href":"http://feedproxy.google.com/~r/alistapart/main/~3/R4Jkp4VLCKM/","type":"text/html"}],"canonical":[{"href":"https://alistapart.com/article/the-career-management-document/","type":"text/html"}],"crawled":1569829757486,"published":1569829757486,"origin":{"streamId":"feed/http://feeds.feedburner.com/alistapart/main","title":"A List Apart: The Full Feed","htmlUrl":"https://alistapart.com"},"visual":{"url":"http://www.blogcdn.com/www.engadget.com/media/2013/10/nvidia-shield-console-mode.jpg","width":620,"height":340,"contentType":"image/jpg"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/885f2e01-d314-4e63-abac-17dcb063f5b5","label":"Programming"}]},{"id":"Yn8gu9QqU/Nu8FbPliaB+X/354R2g5xNVMORFC3HITI=_16d81234d6e:1c4c:90d684ff","keywords":["Accessibility"],"originId":"https://alistapart.com/article/getting-to-the-heart-of-digital-accessibility/","fingerprint":"5a338ace","title":"Getting to the Heart of Digital Accessibility","author":"by Carie Fisher","summary":{"content":"\nQuick! Think of the word “developer” or “coder” — what’s the first thing that comes to mind? Maybe a whiteish male in his twenties living in a busy metropolis, wearing a nerdy t-shirt and hoodie? Someone a bit like Mark Zuckerberg? Or maybe a younger Bill Gates or Sergey Brin? Any of the dudes from the HBO series Silicon Valley, perhaps? Certainly no one like me.
\n\n\nBy tech standards, I’m old. I’m also female and a mother. I live in a midwestern town you’ve never heard of and will never visit — a town where the cows vastly outnumber the people. My hair color is (almost) natural and is no longer part of the ROYGBIV collection, so I have no perceived conference street cred. I own about a thousand geeky T-shirts, but never actually wear them in public, opting for more “girly” attire (or so was pointed out by a male colleague). On the surface, I look more suited to taking notes at a PTA meeting than writing code. I’m a bit of an outsider. A tech misfit.
\n\n\nSo when my 11-year-old daughter finished her recent coding camp and excitedly declared, “Now I’m a real developer, Mom, just like you!” there was the usual parent pride, but also a small piece of me that cringed. Because, as much as I support the STEM fields, and want the next generation of girls to be coding wizard-unicorn-ninjas, I really don’t want my own daughter to be a developer. The rationale behind this bold (and maybe controversial) statement comes from a place of protection. The tech world we live in today is far from perfect. I’ve endured my share of misogyny, self-doubt, and sexual harassment. Why wouldn’t I want to protect her from all of that?
\n\n\nThe (diversity) elephant in the (computer) room
\n\n\nYou’ve heard this story before: there is not enough diversity in tech. This puzzling trend seems to continue year after year, even though numerous studies show that by including more people from underrepresented communities, a company can increase its innovation, employee retention, and bottom line. Even with the recent push and supposed support for diversity and inclusivity from many Fortune 500 companies, women and female-identifying people still only hold 20% of all top tech jobs.
\n\n\nThe data from FY 2018 shows that the number of women in technical roles at three of the top tech giants was 24% for Adobe, 26% for Google, and 22% for Facebook. While these numbers show that there is still not enough representation for women, these numbers do reflect a slight increase from the previous year (FY 2017: Adobe 22%, Google 25%, Facebook 15%). But even with this upward trend of hiring women in tech roles, the marginal growth rate has not caught up with the real world. The tech workforce is seriously out of touch with reality if, in 2019, a demographic (women) that represents more than half the global population is still considered a minority.
\n\n\nSometimes this lack of diversity at the top level is blamed on a “pipeline” issue. The logic being: “If there are not enough girls who learn to code, then there will not be enough women who can code.” However, programs aimed at teaching girls how to code have skyrocketed in the past few years. Girls now make up about half of the enrollment in high-school coding classes and are scoring almost identically to their male classmates on standardized math and science tests, yet, young women make up only 18% of all Computer Science degrees. I have to wonder if this steep drop in interest has more to do with lack of representation in the tech sphere, than with girls and young women simply not being “smart enough” or “not interested” in working with code? At the very least, the lack of representation certainly doesn’t help.
\n\n\nOf course, the diversity picture becomes even more abysmal when you consider other underrepresented groups such as people of color, people from the LGBTQ community, and people with disabilities. And while I really don’t like glossing over these deeper diversity issues in tech, because they are abundant and are much more grotesque failings than the male/female ratio, I also don’t feel qualified to speak about these issues. I encourage you to look to and value the voices of others who can speak with higher authority on these deeper diversity issues, such as Ire Aderinokun, Taelur Alexis, Imani Barbarin, Angie Jones, Fatima Khalid, Tatiana Mac, Charlie Owen, Cherry Rae, and so many others. And for those readers who are new to the topic of diversity in tech, watch Tatiana Mac’s recent conference talk How Privilege Defines Performance — it’s well worth the 35 minutes of your life.
\n\n\nThe four stages in the digital accessibility journey
\n\n\nHowever you look at it, the numbers don’t lie. There are some pretty significant diversity issues in tech. So how do we fix this issue before the next wave of young developers join the tech workforce? Simple: teach developers to write accessible code.
\n\n\nThis may seem like a joke to some and stretch to others, but hear me out. When we talk about accessible code, what we are really talking about at its core is inclusiveness. The actual process of writing accessible code involves rules and standards, tests and tools; but inclusive development is more abstract than that. It’s a shift in thinking. And when we rethink our approach to development, we go beyond just the base level of simple code functionality. We instead think, how is this code consumed? How can we make it even more intelligible and easier for people to use? Inclusive development means making something valuable, not just accessible, to as many people as we can.
\n\n\nThat line of thinking is a bit abstract, so let’s go through an example. Let’s say you are tasked with updating the color contrast between the text on a webpage or app and the background. What happens at each stage in the accessibility journey?
\n\n\nStage 1: Awareness — You are brand new to digital accessibility and are still trying to understand what it is and how you can implement changes in your daily workflow. You may be aware that there is a set of digital accessibility guidelines that other developers follow, but you are a bit hazy on what it all means in a practical sense.
\n\n\nStage 2: Knowledge — You know a bit more about digital accessibility and feel comfortable using a few testing tools, so you run an automated accessibility test on your website and it flags a possible issue with the color contrast. Based on your awareness of the guidelines, you know the color contrast ratio between the text and the background needs to be a certain number and that you need a tool to test this.
\n\n\nStage 3: Practice — Feeling more confident in your knowledge of digital accessibility rules and best practices, you use a tool to measure the color contrast ratio between the text and the background. Then based on the output of the tool, you modify the hex code to meet the color contrast ratio guidelines and retest to confirm you have met the accessibility requirements for this issue.
\n\n\nStage 4: Understanding — You understand that the accessibility guidelines and tools are created with people in mind, and that code is secondary to all of that. One is the means, and the other is the end. In the color contrast example, you understand that people with low-vision or colorblindness need these color contrast changes in order to actually see the words on your web page.
\n\n\nThis is a bit of an oversimplification of the process. But I hope you get the gist — that there are different stages of digital accessibility knowledge and understanding. True beginners may not be to even stage one, but I am finding that group rarer and rarer these days. The word about digital accessibility seems to be out! Which is great; but that’s only the first hurdle. What I’m seeing now is that a lot of people stop at Stage 2: Knowledge or Stage 3: Practice — where you are aware of the digital accessibility guidelines, have some testing tools in your back pocket, and know how to fix some of the issues reported, but haven’t quite connected the dots to the humans they impact.
\n\n\nFrom the standpoint of getting daily stuff done, stages two and three are okay stopping points. But what happens when the things you need to do are too complex for a quick fix, or you have no buy-in from your peers or management? I feel that once we get to Stage 4: Understanding, and really get why these kinds of changes are needed, people will be more motivated to make those changes regardless of the challenges involved. When you arrive at stage four, you have gone beyond knowing the basic rules, testing, and coding. You recognize that digital accessibility is not just a “nice to have” but a “must have” and it becomes about quality of life for real people. This is digital inclusion. This is something you can’t unsee, you can’t unlearn, and you can’t ignore.
\n\n\nMaking digital accessibility a priority — not a requirement
\n\n\nIn my role as an accessibility trainer, I like to kick-off each session with the question: “What are you hoping to learn today about digital accessibility?” I ask this question to establish a rapport with the audience and to understand where everyone is in their accessibility journey, but I am also evaluating the level of company and individual buy-in too. There is nothing worse than showing up to teach a group that does not care to be taught. If I hear the words “I am only here because I have to be” — I know it will be an uphill battle to get them anywhere close to Stage 4: Understanding, so I mentally regroup and aim for another stage.
\n\n\nIn my experience, when companies and their leaders say “Digital accessibility is a requirement,” nine times out of ten there is a motivating factor behind this sweeping declaration (for example, impending litigation, or at least the fear of it). When changes are framed as mandatory and packaged as directives from on high with little additional context, people can be resistant and will find excuses to fight or challenge the declaration, and any change can become an uphill battle. Calling something “mandatory” only speaks to Stage 1: Awareness.
\n\n\nBy swapping out one word from the original declaration and saying “Digital accessibility is a priority,” companies and their leaders have reframed the conversation with their employees. When changes are framed as “working towards a solution” and discussed openly and collaboratively, people feel like they are part of the process and are more open to embracing change. In the long run, embracing change becomes part of a company’s culture and leads to innovation (and, yes, inclusion) on all levels. Calling something a priority speaks to Stage 4: Understanding.
\n\n\nSome of the excuses I often hear from clients for not prioritizing accessibility is that it is too difficult, too costly, and/or too time consuming — but is that really the case? In the same accessibility training, I lead an exercise where we look at a website with an accessibility testing tool and review any issues that came up. With the group’s help we plot out the “impact to user” versus the “remediation effort” on the part of the team. From group to group, while the plots are slightly different, one commonality is that close to 80% of the errors plotted fall into the quadrant of “simple to fix” for the team, but they also fall under “high impact” to the user. Based on this empirical data, I won’t buy the argument from clients who say that accessibility is too difficult and costly and time consuming anymore. It comes down to whether it’s a priority — for each individual and for the company as a whole.
\n\n\nWhat will your coding legacy be?
\n\n\nThe infinite monkey theorem states that a monkey hitting keys at random on a typewriter for an infinite amount of time will eventually type any given text, such as the complete works of William Shakespeare. So by that same logic, a programmer hitting keys at random on a computer for an infinite amount of time will almost surely produce a website that is accessible. But where is the thought process? Where is the human element? While all the things we’ve already talked about — awareness, education, and prioritization of accessibility are important steps in making the digital world more inclusive to all — without intent, we are just going to keep randomly tapping away at our computers, repeating the same mistakes over and over again. The intent behind the code has to be part of the process, otherwise accessibility is just another task that has no meaning.
\n\n\nMaybe I’m naive, but I’d like to think we’ve come to a point in our society where we want our work lives to have meaning. And that we don’t want to just hear about the positive change that is happening, but want to be part of the change. Digital accessibility is a place where this can happen! Not only does understanding and writing purpose-driven code help people with disabilities in the short-run, I believe strongly that is key to solving the overarching diversity issue in tech in the long-run. Developers who reach Stage 4: Understanding, and who prioritize accessible code because they understand it’s fundamentally about people, will also be the ones who help create and cultivate an inclusive environment where people from more diverse backgrounds are also prioritized and accepted in the tech world.
\n\n\nBecause when you strip away all the styles, all the mark-up, all the cool features from a website or app — what’s left? People. And honestly, the more I learn about digital accessibility, the more I realize it’s not about the code at all. Digital accessibility is rooted in the user; and, while I (and countless others) can certainly teach you how to write accessible code, and build you tools, patterns, and libraries to use, I realize we can’t teach you to care. That is a choice you have to make yourself. So think for a moment — what are you leaving the next generation of developers with all that inaccessible code you haven’t given much thought to? Is it the coding legacy you really want to leave? I challenge you to do better for my daughter, her peers, and for the countless others who are not fully represented in the tech community today.
\n
","direction":"ltr"},"alternate":[{"href":"http://feedproxy.google.com/~r/alistapart/main/~3/GeRjOSU3AQc/","type":"text/html"}],"canonical":[{"href":"https://alistapart.com/article/getting-to-the-heart-of-digital-accessibility/","type":"text/html"}],"crawled":1569829637486,"published":1569829637486,"origin":{"streamId":"feed/http://feeds.feedburner.com/alistapart/main","title":"A List Apart: The Full Feed","htmlUrl":"https://alistapart.com"},"visual":{"url":"http://www.blogcdn.com/www.engadget.com/media/2013/10/nvidia-shield-console-mode.jpg","width":620,"height":340,"contentType":"image/jpg"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/885f2e01-d314-4e63-abac-17dcb063f5b5","label":"Programming"}]},{"id":"Yn8gu9QqU/Nu8FbPliaB+X/354R2g5xNVMORFC3HITI=_16d812178ae:1c4b:90d684ff","keywords":["Application Development, Browsers, JavaScript"],"originId":"https://alistapart.com/article/responsible-javascript-part-2/","fingerprint":"9085fc74","title":"Responsible JavaScript: Part II","author":"by Jeremy Wagner","summary":{"content":"\nYou and the rest of the dev team lobbied enthusiastically for a total re-architecture of the company’s aging website. Your pleas were heard by management—even up to the C-suite—who gave the green light. Elated, you and the team started working with the design, copy, and IA teams. Before long, you were banging out new code.
\n\n\nIt started out innocently enough with an npm install here and an npm install there. Before you knew it, though, you were installing production dependencies like an undergrad doing keg stands without a care for the morning after.
\n\n\nThen you launched.
\n\n\nUnlike the aftermath of most copious boozings, the agony didn’t start the morning after. Oh, no. It came months later in the ghastly form of low-grade nausea and headache of product owners and middle management wondering why conversions and revenue were both down since the launch. It then hit a fever pitch when the CTO came back from a weekend at the cabin and wondered why the site loaded so slowly on their phone—if it indeed ever loaded at all.
\n\n\nEveryone was happy. Now no one is happy. Welcome to your first JavaScript hangover.
\n\n\nIt’s not your fault
\n\n\nWhen you’re grappling with a vicious hangover, “I told you so” would be a well-deserved, if fight-provoking, rebuke—assuming you could even fight in so sorry a state.
\n\n\nWhen it comes to JavaScript hangovers, there’s plenty of blame to dole out. Pointing fingers is a waste of time, though. The landscape of the web today demands that we iterate faster than our competitors. This kind of pressure means we’re likely to take advantage of any means available to be as productive as possible. That means we’re more likely—but not necessarily doomed—to build apps with more overhead, and possibly use patterns that can hurt performance and accessibility.
\n\n\nWeb development isn't easy. It’s a long slog we rarely get right on the first try. The best part of working on the web, however, is that we don’t have to get it perfect at the start. We can make improvements after the fact, and that’s just what the second installment of this series is here for. Perfection is a long ways off. For now, let’s take the edge off of that JavaScript hangover by improving your site’s, er, scriptuation in the short term.
\n\n\nRound up the usual suspects
\n\n\nIt might seem rote, but it’s worth going through the list of basic optimizations. It’s not uncommon for large development teams—particularly those that work across many repositories or don’t use optimized boilerplate—to overlook them.
\n\n\nShake those trees
\n\n\nFirst, make sure your toolchain is configured to perform tree shaking. If tree shaking is new to you, I wrote a guide on it last year you can consult. The short of it is that tree shaking is a process in which unused exports in your codebase don’t get packaged up in your production bundles.
\n\n\nTree shaking is available out of the box with modern bundlers such as webpack, Rollup, or Parcel. Grunt or gulp—which are not bundlers, but rather task runners—won’t do this for you. A task runner doesn’t build a dependency graph like a bundler does. Rather, they perform discrete tasks on the files you feed to them with any number of plugins. Task runners can be extended with plugins to use bundlers to process JavaScript. If extending task runners in this way is problematic for you, you’ll likely need to manually audit and remove unused code.
\n\n\nFor tree shaking to be effective, the following must be true:
\n\n\n- Your app logic and the packages you install in your project must be authored as ES6 modules. Tree shaking CommonJS modules isn’t practically possible.
- Your bundler must not transform ES6 modules into another module format at build time. If this happens in a toolchain that uses Babel, @babel/preset-env configuration must specify
modules: false to prevent ES6 code from being converted to CommonJS.
\n\n\nOn the off chance tree shaking isn’t occurring during your build, getting it to work may help. Of course, its effectiveness varies on a case-by-case basis. It also depends on whether the modules you import introduce side effects, which may influence a bundler’s ability to shake unused exports.
\n\n\nSplit that code
\n\n\nChances are good that you’re employing some form of code splitting, but it’s worth re-evaluating how you’re doing it. No matter how you’re splitting code, there are two questions that are always worth asking yourself:
\n\n\n- Are you deduplicating common code between entry points?
- Are you lazy loading all the functionality you reasonably can with dynamic
import()?
\n\n\nThese are important because reducing redundant code is essential to performance. Lazy loading functionality also improves performance by lowering the initial JavaScript footprint on a given page. On the redundancy front, using an analysis tool such as Bundle Buddy can help you find out if you have a problem.
\n\n\n
Bundle Buddy can examine your webpack compilation statistics and determine how much code is shared between your bundles.\n\n\nWhere lazy loading is concerned, it can be a bit difficult to know where to start looking for opportunities. When I look for opportunities in existing projects, I’ll search for user interaction points throughout the codebase, such as click and keyboard events, and similar candidates. Any code that requires a user interaction to run is a potentially good candidate for dynamic import().
\n\n\nOf course, loading scripts on demand brings the possibility that interactivity could be noticeably delayed, as the script necessary for the interaction must be downloaded first. If data usage is not a concern, consider using the rel=prefetch resource hint to load such scripts at a low priority that won’t contend for bandwidth against critical resources. Support for rel=prefetch is good, but nothing will break if it’s unsupported, as such browsers will ignore markup they doesn’t understand.
\n\n\nExternalize third-party hosted code
\n\n\nIdeally, you should self-host as many of your site’s dependencies as possible. If for some reason you must load dependencies from a third party, mark them as externals in your bundler’s configuration. Failing to do so could mean your website’s visitors will download both locally hosted code and the same code from a third party.
\n\n\nLet’s look at a hypothetical situation where this could hurt you: say that your site loads Lodash from a public CDN. You've also installed Lodash in your project for local development. However, if you fail to mark Lodash as external, your production code will end up loading a third party copy of it in addition to the bundled, locally hosted copy.
\n\n\nThis may seem like common knowledge if you know your way around bundlers, but I’ve seen it get overlooked. It’s worth your time to check twice.
\n\n\nIf you aren’t convinced to self-host your third-party dependencies, then consider adding dns-prefetch, preconnect, or possibly even preload hints for them. Doing so can lower your site’s Time to Interactive and—if JavaScript is critical to rendering content—your site’s Speed Index.
\n\n\nSmaller alternatives for less overhead
\n\n\nUserland JavaScript is like an obscenely massive candy store, and we as developers are awed by the sheer amount of open source offerings. Frameworks and libraries allow us to extend our applications to quickly do all sorts of stuff that would otherwise take loads of time and effort.
\n\n\nWhile I personally prefer to aggressively minimize the use of client-side frameworks and libraries in my projects, their value is compelling. Yet, we do have a responsibility to be a bit hawkish when it comes to what we install. When we’ve already built and shipped something that depends on a slew of installed code to run, we’ve accepted a baseline cost that only the maintainers of that code can practically address. Right?
\n\n\nMaybe, but then again, maybe not. It depends on the dependencies used. For instance, React is extremely popular, but Preact is an ultra-small alternative that largely shares the same API and retains compatibility with many React add-ons. Luxon and date-fns are much more compact alternatives to moment.js, which is not exactly tiny.
\n\n\nLibraries such as Lodash offer many useful methods. Yet, some of them are easily replaceable with native ES6. Lodash’s compact method, for example, is replaceable with the filter array method. Many more can be replaced without much effort, and without the need for pulling in a large utility library.
\n\n\nWhatever your preferred tools are, the idea is the same: do some research to see if there are smaller alternatives, or if native language features can do the trick. You may be surprised at how little effort it may take you to seriously reduce your app’s overhead.
\n\n\nDifferentially serve your scripts
\n\n\nThere’s a good chance you’re using Babel in your toolchain to transform your ES6 source into code that can run on older browsers. Does this mean we’re doomed to serve giant bundles even to browsers that don’t need them, until the older browsers disappear altogether? Of course not! Differential serving helps us get around this by generating two different builds of your ES6 source:
\n\n\n- Bundle one, which contains all the transforms and polyfills required for your site to work on older browsers. You’re probably already serving this bundle right now.
- Bundle two, which contains little to none of the transforms and polyfills because it targets modern browsers. This is the bundle you’re probably not serving—at least not yet.
\n\n\nAchieving this is a bit involved. I’ve written a guide on one way you can do it, so there’s no need for a deep dive here. The long and short of it is that you can modify your build configuration to generate an additional but smaller version of your site’s JavaScript code, and serve it only to modern browsers. The best part is that these are savings you can achieve without sacrificing any features or functionality you already offer. Depending on your application code, the savings could be quite significant.
\n\n\n
A webpack-bundle-analyzer analysis of a project's legacy bundle (left) versus one for a modern bundle (right). View full-sized image.\n\n\nThe simplest pattern for serving these bundles to their respective platforms is brief. It also works a treat in modern browsers:
\n\n\n<!-- Modern browsers load this file: -->\n<script type="module" src="/js/app.mjs"></script>\n<!-- Legacy browsers load this file: -->\n<script defer nomodule src="/js/app.js"></script>
\n\n\nUnfortunately, there’s a caveat with this pattern: legacy browsers like IE 11—and even relatively modern ones such as Edge versions 15 through 18—will download both bundles. If this is an acceptable trade-off for you, then worry no further.
\n\n\nOn the other hand, you'll need a workaround if you’re concerned about the performance implications of older browsers downloading both sets of bundles. Here’s one potential solution that uses script injection (instead of the script tags above) to avoid double downloads on affected browsers:
\n\n\nvar scriptEl = document.createElement("script");\n\nif ("noModule" in scriptEl) {\n // Set up modern script\n scriptEl.src = "/js/app.mjs";\n scriptEl.type = "module";\n} else {\n // Set up legacy script\n scriptEl.src = "/js/app.js";\n scriptEl.defer = true; // type="module" defers by default, so set it here.\n}\n\n// Inject!\ndocument.body.appendChild(scriptEl);
\n\n\nThis script infers that if a browser supports the nomodule attribute in the script element, it understands type="module". This ensures that legacy browsers only get legacy scripts and modern browsers only get modern ones. Be warned, though, that dynamically injected scripts load asynchronously by default, so set the async attribute to false if dependency order is crucial.
\n\n\nTranspile less
\n\n\nI’m not here to trash Babel. It’s indispensable, but lordy, it adds a lot of extra stuff without your ever knowing. It pays to peek under the hood to see what it’s up to. Some minor changes in your coding habits can have a positive impact on what Babel spits out.
\n\n\n
https://twitter.com/_developit/status/1110229993999777793\n\n\nTo wit: default parameters are a very handy ES6 feature you probably already use:
\n\n\nfunction logger(message, level = "log") {\n console[level](message);\n}
\n\n\nThe thing to pay attention to here is the level parameter, which has a default of “log.” This means if we want to invoke console.log with this wrapper function, we don’t need to specify level. Great, right? Except when Babel transforms this function, the output looks like this:
\n\n\nfunction logger(message) {\n var level = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "log";\n\n console[level](message);\n}
\n\n\nThis is an example of how, despite our best intentions, developer conveniences can backfire. What was a handful of bytes in our source has now been transformed into much larger in our production code. Uglification can’t do much about it either, as arguments can’t be reduced. Oh, and if you think rest parameters might be a worthy antidote, Babel’s transforms for them are even bulkier:
\n\n\n// Source\nfunction logger(...args) {\n const [level, message] = args;\n\n console[level](message);\n}\n\n// Babel output\nfunction logger() {\n for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {\n args[_key] = arguments[_key];\n }\n\n const level = args[0],\n message = args[1];\n console[level](message);\n}
\n\n\nWorse yet, Babel transforms this code even for projects with a @babel/preset-env configuration targeting modern browsers, meaning the modern bundles in your differentially served JavaScript will be affected too! You could use loose transforms to soften the blow—and that’s a fine idea, as they’re often quite a bit smaller than their more spec-compliant counterparts—but enabling loose transforms can cause issues if you remove Babel from your build pipeline later on.
\n\n\nRegardless of whether you decide to enable loose transforms, here’s one way to cut the cruft of transpiled default parameters:
\n\n\n// Babel won't touch this\nfunction logger(message, level) {\n console[level || "log"](message);\n}
\n\n\nOf course, default parameters aren’t the only feature to be wary of. For example, spread syntax gets transformed, as do arrow functions and a whole host of other stuff.
\n\n\nIf you don’t want to avoid these features altogether, you have a couple ways of reducing their impact:
\n\n\n- If you’re authoring a library, consider using @babel/runtime in concert with @babel/plugin-transform-runtime to deduplicate the helper functions Babel puts into your code.
- For polyfilled features in apps, you can include them selectively with @babel/polyfill via @babel/preset-env’s useBuiltIns: "usage" option.
\n\n\nThis is solely my opinion, but I believe the best choice is to avoid transpilation altogether in bundles generated for modern browsers. That’s not always possible, especially if you use JSX, which must be transformed for all browsers, or if you’re using bleeding edge language features that aren’t widely supported. In the latter case, it might be worth asking if those features are really necessary to deliver a good user experience (they rarely are). If you arrive at the conclusion that Babel must be a part of your toolchain, then it’s worth peeking under the hood from time to time to catch suboptimal stuff Babel might be doing that you can improve on.
\n\n\nImprovement is not a race
\n\n\nAs you massage your temples wondering when this horrid JavaScript hangover is going to lift, understand that it’s precisely when we rush to get something out there as fast as we possibly can that the user experience can suffer. As the web development community obsesses on iterating faster in the name of competition, it’s worth your time to slow down a little bit. You’ll find that by doing so, you may not be iterating as fast as your competitors, but your product will be faster than theirs.
\n\n\nAs you take these suggestions and apply them to your codebase, know that progress doesn’t spontaneously happen overnight. Web development is a job. The truly impactful work is done when we’re thoughtful and dedicated to the craft for the long haul. Focus on steady improvements. Measure, test, repeat, and your site’s user experience will improve, and you’ll get faster bit by bit over time.
\n\n\nSpecial thanks to Jason Miller for tech editing this piece. Jason is the creator and one of the many maintainers of Preact, a vastly smaller alternative to React with the same API. If you use Preact, please consider supporting Preact through Open Collective.
\n
","direction":"ltr"},"alternate":[{"href":"http://feedproxy.google.com/~r/alistapart/main/~3/66zevQGQ6YM/","type":"text/html"}],"canonical":[{"href":"https://alistapart.com/article/responsible-javascript-part-2/","type":"text/html"}],"crawled":1569829517486,"published":1569829517486,"origin":{"streamId":"feed/http://feeds.feedburner.com/alistapart/main","title":"A List Apart: The Full Feed","htmlUrl":"https://alistapart.com"},"visual":{"url":"http://www.blogcdn.com/www.engadget.com/media/2013/10/nvidia-shield-console-mode.jpg","width":620,"height":340,"contentType":"image/jpg"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/885f2e01-d314-4e63-abac-17dcb063f5b5","label":"Programming"}]},{"id":"Yn8gu9QqU/Nu8FbPliaB+X/354R2g5xNVMORFC3HITI=_16d811fa3ee:1c4a:90d684ff","keywords":["Business, Career"],"originId":"https://alistapart.com/article/resilient-management-excerpt/","fingerprint":"4c7dfe28","title":"Resilient Management, An Excerpt","author":"by Lara Hogan","summary":{"content":"\nIn Tuckman’s Stages of Group Development, the Storming stage happens as a group begins to figure out how to work together. Previously, each person had been doing their own thing as individuals, so necessarily a few things need to be ironed out: how to collaborate, how to hit goals, how to determine priorities. Of course there may be some friction here!
\n\n\nBut even if your team doesn’t noticeably demonstrate this kind of internal Storming as they begin to gel, there might be some outside factors at play in your work environment that create friction. During times of team scaling and organizational change—the water we in the web industry are often swimming in—managers are responsible for things like strategy-setting, aligning their team’s work to company objectives, and unblocking the team as they ship their work.
\n\n\nIn addition to these business-context responsibilities, managers need to be able to help their teammates navigate this storm by helping them grow in their roles and support the team’s overall progress. If you and your teammates don’t adapt and evolve in your roles, it’s unlikely that your team will move out of the Storming stage and into the Norming stage of team dynamics.
\n\n\nTo spur this course-correction and growth in your teammates, you’ll end up wearing four different hats:
\n\n\n- Mentoring: lending advice and helping to problem solve based on your own experience.
- Coaching: asking open questions to help your teammate reflect and introspect, rather than sharing your own opinions or quickly problem solving.
- Sponsoring: finding opportunities for your teammate to level up, take on new leadership roles, and get promoted.
- Delivering feedback: observing behavior that is or isn’t aligned to what the team needs to be doing and sharing those observations, along with praise or suggestions.
\n\n\nLet’s dive in to how to choose, and when to use, each of\nthese skills as you grow your teammates, and then talk about\nwhat it looks like when teammates support the overarching\ndirection of the team.\n
\n\n\nMentoring
\n\n\nWhen I talk to managers, I find that the vast majority have their\nmentor hats on ninety percent of the time when they’re working with their\nteammates. It’s natural!
\n\n\nIn mentoring mode, we’re doling out advice, sharing our perspective,\nand helping someone else problem solve based on that information. Our personal\nexperiences are often what we can talk most confidently about! For this reason,\nmentorship mode can feel really good and effective for the mentor. Having that\nmentor hat on can help the other person overcome a roadblock or know which next\nsteps to take, while avoiding drastic errors that they wouldn’t have seen\ncoming otherwise.
\n\n\nAs a mentor, it’s your responsibility to give advice that’s current and sensitive to the changing dialog happening in our industry. Advice that might work for one person (“Be louder in meetings!” or “Ask your boss for a raise!”) may undermine someone else, because members of underrepresented groups are unconsciously assessed and treated differently. For example, research has shown that “when women are collaborative and communal, they are not perceived as competent—but when they emphasize their competence, they’re seen as cold and unlikable, in a classic ‘double bind’”.
\n\n\nIf you are not a member of a marginalized group, and you have a\nmentee who is, please be a responsible mentor! Try to be aware of the\nway members of underrepresented groups are perceived, and the unconscious bias\nthat might be at play in your mentee’s work environment. When you have your\nmentor hat on, do lots of gut checking to make sure that your advice is going\nto be helpful in practice for your mentee.
\n\n\nMentoring is ideal when the mentee is new to their role or to the\norganization; they need to learn the ropes from someone who has firsthand\nexperience. It’s also ideal when your teammate is working on a problem and has\ntried out a few different approaches, but still feels stumped; this is why\npractices like pair coding can help folks learn new things.
\n\n\nAs mentors, we want our mentees to reach beyond us, because our mentees’ success is ultimately our success. Mentorship relationships evolve over time, because each party is growing. Imaginative, innovative ideas often come from people who have never seen a particular challenge before, so if your mentee comes up with a creative solution on their own that you wouldn’t have thought of, be excited for them—don’t just focus on the ways that you’ve done it or seen it done before.
\n\n\nManagers often default to mentoring mode because it feels like the fastest way to solve a problem, but it falls short in helping your teammate connect their own dots. For that, we’ll look to coaching.
\n\n\nCoaching
\n\n\nIn mentoring mode, you’re focused on both the problem and the solution. You’ll share what you as the mentor would do or have done in this situation. This means you’re more focused on yourself, and less on the person who is sitting in front of you.
\n\n\nIn coaching mode—an extremely powerful but often underutilized mode—you’re doing two primary things:
\n\n\n- Asking open questions to help the other person explore more of the shape of the topic, rather than staying at the surface level.
- Reflecting, which is like holding up a mirror for the other person and describing what you see or hear, or asking them to reflect for themselves.
\n\n\n These two tools will help you become your teammate’s fiercest champion.
\n\n\nOpen Questions
\n\n\n“Closed” questions can only be answered with yes or no.\nOpen questions often start with who, what, when, where,\nwhy, and how. But the best open questions are about the\nproblem, not the solution. Questions that start with why tend to make\nthe other person feel judged, and questions that start with how tend\nto go into problem solving mode—both of which we want to avoid while in\ncoaching mode.
\n\n\nHowever, what questions can be authentically curious!\nWhen someone comes to you with a challenge, try asking questions like:
\n\n\n- What’s most important to you about it?
- What’s holding you back?
- What does success look like?
\n\n\nLet’s say my teammate comes to me and says they’re ready for a\npromotion. Open questions could help this teammate explore what this promotion\nmeans and demonstrate to me what introspection they’ve already done around it.\nRather than telling them what I think is necessary for them to be promoted, I\ncould instead open up this conversation by asking them:
\n\n\n- What would you be able to do in the new level\nthat you can’t do in your current one?
- What skills are required in the new level? What\nare some ways that you’ve honed those skills?
- Who are the people already at that level that\nyou want to emulate? What about them do you want to emulate?
\n\n\n Their answers would give me a place to start coaching. These questions might push my teammate to think more deeply about what this promotion means, rather than allowing them to stay surface level and believe that a promotion is about checking off a lot of boxes on a list. Their answers might also open my eyes to things that I hadn’t seen before, like a piece of work that my teammate had accomplished that made a huge impact. But most important, going into coaching mode would start a two-way conversation with this teammate, which would help make an otherwise tricky conversation feel more like a shared exploration.
\n\n\nOpen\nquestions, asked from a place of genuine curiosity, help people feel seen and\nheard. However, if the way you ask your questions comes across as judgy or like\nyou’ve already made some assumptions, then your questions aren’t truly open\n(and your teammate can smell this on you!). Practice your intonation to make\nsure your open questions are actually curious and open.
\n\n\nBy the way, forming lots of open questions (instead of problem solving questions, or giving advice) is tremendously hard for most people. Don’t worry if you don’t get the hang of it at first; it takes a lot of practice and intention over time to default to coaching mode rather than mentoring mode. I promise, it’s worth it.
\n\n\nReflections
\n\n\nJust like open questions, reflections help the other person feel\nseen and heard, and to explore the topic more deeply.
\n\n\nIt’s almost comical how rarely we get the sense that the person\nwe’re talking to is actively listening to us, or focusing entirely on helping\nus connect our own dots. Help your teammates reflect by repeating back to them\nwhat you hear them say, as in:
\n\n\n- “What I’m hearing you say is that you’re\nfrustrated with how this project is going. Is that right?”
- “What I know to be true about you is how deeply\nyou care about your teammates’ feelings.”
\n\n\nIn each of these\nexamples, you are holding up a metaphorical mirror to your teammate, and\nhelping them look into it. You can coach them to reflect, too:
\n\n\n- “How does this new architecture project map to\nyour goals?”
- “Let’s reflect on where you were this time last\nyear and how far you’ve come.”
\n\n\nOccasionally, you might get a reflection wrong; this gives the\nother person an opportunity to realize something new about their topic, like\nthe words they’re choosing aren’t quite right, or there’s another underlying\nissue that should be explored. So don’t be worried about giving a bad\nreflection; reflecting back what you’re hearing will still help your teammate.
\n\n\nThe act of reflecting can help the other person do a gut check to\nmake sure they’re approaching their topic holistically. Sometimes the act of\nreflection forces (encourages?) the other person to do some really hard work: introspection.\nIntrospection creates an opportunity for them to realize new aspects of the\nproblem, options they can choose from, or deeper meanings that hadn’t occurred\nto them before—which often ends up being a nice shortcut to the right solution.\nOr, even better, the right problem statement.
\n\n\nWhen you have your coaching hat on, you don’t need to have all the answers, or even fully understand the problem that your teammate is wrestling with; you’re just there as a mirror and as a question-asker, to help prompt the other person to think deeply and come to some new, interesting conclusions. Frankly, it may not feel all that effective when you’re in coaching mode, but I promise, coaching can generate way more growth for that other person than just giving them advice or sharing your perspective.
\n\n\nChoose coaching when you’re looking to help someone (especially an emerging leader) hone their strategic thinking skills, grow their leadership aptitude, and craft their own path forward. Coaching mode is all about helping your teammate develop their own brain wrinkles, rather than telling them how you would do something. The introspection and creativity it inspires create deeper and longer-lasting growth.
\n\n\nSponsoring
\n\n\nWhile you wear the mentoring and coaching hats around your\nteammates, the sponsor hat is more often worn when they’re not around,\nlike when you’re in a 1:1 with your manager, a sprint planning meeting, or\nanother environment where someone’s work might be recognized. You might hear\nabout an upcoming project to acquire a new audience and recommend that a\nbudding user researcher take it on, or you’ll suggest to an All Hands meeting\norganizer that a junior designer should give a talk about a new pattern they’ve\nintroduced to the style guide.
\n\n\nSponsorship is all about feeling on the hook for getting\nsomeone to the next level. As someone’s sponsor, you’ll put their name in the\nring for opportunities that will get them the experience and visibility\nnecessary to grow in their role and at the organization. You will put your\npersonal reputation on the line on behalf of the person you’re sponsoring, to\nhelp get them visible and developmental assignments. It’s a powerful tool, and\nthe one most effective at helping someone get to the next level (way more so\nthan mentoring or coaching!).
\n\n\nThe Center for Talent Innovation routinely measures the career benefits of sponsorship (PDF). Their studies have found that when someone has a sponsor, they are way more likely to have access to career-launching work. They’re also more likely to take actions that lead to even more growth and opportunities, like asking their manager for a stretch assignment or a raise.
\n\n\nWhen you’re in sponsorship mode, think about the different\nopportunities you have to offer up someone’s name. This might look like:
\n\n\n- giving visible/public recognition (company\n“shout outs,” having them present a project demo, thanking them in a launch\nemail, giving someone’s manager feedback about their good work);
- assigning stretch tasks and projects that are just\nbeyond their current skill set, to help them grow and have supporting\nevidence for a future promotion; or
- opening the door for them to write blog posts,\ngive company or conference talks, or contribute open-source work.
\n\n\nRemember that members of underrepresented groups are typically over-mentored, but under-sponsored. These individuals get lots of advice (often unsolicited), coffee outings, and offers to teach them new skills. But it’s much rarer for them to see support that looks like sponsorship.
\n\n\nThis isn’t because sponsors intentionally ignore marginalized folks, but because of in-group bias. Because of how our brains (and social networks) work, the people we’re closest to tend to look mostly like us—and we draw from that same pool when we nominate people for projects, for promotions, and for hires. Until I started learning about bias in the workplace, most of the people I sponsored were white, cisgender women, like myself. Since then, I’ve actively worked to sponsor people of color and nonbinary people. It takes effort and intention to combat our default behaviors—but I know you can do it!
\n\n\nTake a look at the daily communications you participate in: your work chat logs, the conversations you have with others, the process for figuring out who should fix a bug or work on a new project, and the processes for making your teams’ work visible (like an architecture review, code review, launch calendar, etc.). You’ll be surprised how many moments there are to sponsor someone throughout an average day. Please put in the time and intention to ensure that you’re sponsoring members of underrepresented groups, too.
\n
","direction":"ltr"},"alternate":[{"href":"http://feedproxy.google.com/~r/alistapart/main/~3/OONUldza8XM/","type":"text/html"}],"canonical":[{"href":"https://alistapart.com/article/resilient-management-excerpt/","type":"text/html"}],"crawled":1569829397486,"published":1569829397486,"origin":{"streamId":"feed/http://feeds.feedburner.com/alistapart/main","title":"A List Apart: The Full Feed","htmlUrl":"https://alistapart.com"},"visual":{"url":"http://www.blogcdn.com/www.engadget.com/media/2013/10/nvidia-shield-console-mode.jpg","width":620,"height":340,"contentType":"image/jpg"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/885f2e01-d314-4e63-abac-17dcb063f5b5","label":"Programming"}]},{"id":"Yn8gu9QqU/Nu8FbPliaB+X/354R2g5xNVMORFC3HITI=_16d811dcf2e:1c49:90d684ff","keywords":["Design"],"originId":"https://alistapart.com/article/daily-ethical-design/","fingerprint":"c00b5c74","title":"Daily Ethical Design","author":"by Lennart Overkamp","summary":{"content":"Suddenly, I realized that the people next to me might be severely impacted by my work.\nI was having a quick lunch in the airport. A group of flight attendants sat down at the table next to me and started to prepare for their flight. For a while now, our design team had been working on futuristic concepts for the operations control center of these flight attendants’ airline, pushing ourselves to come up with innovative solutions enabled by the newest technologies. As the control center deals with all activities around flying planes, our concepts touched upon everything and everyone within the airline.\nHow was I to know what the impact of my work would be on the lives of these flight attendants? And what about the lives of all the other people working at the airline?\nIdeally, we would have talked to all the types of employees in the company and tested our concepts with them. But, of course, there was no budget (or time) allocated to do so, not to mention we faced the hurdle of convincing (internal) stakeholders of the need.\nNot for the first time, I felt frustrated: practical, real-world constraints prevented me from assessing the impact and quality of my work. They prevented me from properly conducting ethical design.\nWhat is ethical design?
\nRight, good question. A very comprehensive definition of ethical design can be found at Encyclopedia.com:\n\nDesign ethics concerns moral behavior and responsible choices in the practice of design. It guides how designers work with clients, colleagues, and the end users of products, how they conduct the design process, how they determine the features of products, and how they assess the ethical significance or moral worth of the products that result from the activity of designing.
\n\nIn other words, ethical design is about the “goodness”—in terms of benefit to individuals, society, and the world—of how we collaborate, how we practice our work, and what we create. There’s never a black-and-white answer for whether design is good or bad, yet there are a number of areas for designers to focus on when considering ethics.\nUsability
\nNowadays usability has conquered a spot as a basic requirement for each interface; unusable products are considered design failures. And rightly so; we have a moral obligation as designers to create products that are intuitive, safe, and free from possibly life-threatening errors. We were all reminded of usability’s importance by last year’s accidental nuclear strike warning in Hawaii. What if, instead of a false-positive, the operator had broadcasted a false-negative?\nAccessibility
\nLike usability, inclusive design has become a standard item in the requirement list of many designers and companies. (I will never forget that time someone tried to use our website with a screen reader—and got absolutely stuck at the cookie message.) Accessible design benefits all, as it attempts to cover as many needs and capabilities as possible. Yet for each design project, there are still a lot of tricky questions to answer. Who gets to benefit from our solutions? Who is (un)intentionally left out? Who falls outside the “target customer segment”?\nPrivacy
\nAnother day, another Facebook privacy scandal. As we’re progressing into the Data Age, the topic of privacy has become almost synonymous with design ethics. There’s a reason why more and more people use DuckDuckGo as an alternative search engine to Google. Corporations have access to an abundance of personal information about consumers, and as designers we have the privilege—and responsibility—of using this information to shape products and services. We have to consider how much information is strictly necessary and how much people are willing to give up in exchange for services. And how can we make people aware of the potential risks without overloading them?\nUser involvement
\nOverlapping largely with privacy, this focus area is about how we deal with our users and what we do with the data that we collect from them. IDEO has recently published The Little Book of Design Research Ethics, which provides a comprehensive overview of the core principles and guidelines we should follow when conducting design research.\nPersuasion
\nEthics related to persuasion is about to what extent we may influence the behavior and thoughts of our users. It doesn’t take much to bring acceptable, “white hat” persuasion into gray or even dark territories. Conversion optimization, for example, can easily turn into “How do we squeeze out more revenue from our customers by turning their unconsciousness against them?” Prime examples include Netflix, which convinces us to watch, watch, and watch even more, and Booking.com, which barrages our senses with urgency and social pressure.\nFocus
\nThe current digital landscape is addictive, distracting, and competing for attention. Designing for focus is about responsibly handling people’s most valuable resource: time. Our challenge is to limit everything that disrupts our users’ attention, lower the addictiveness of products, and create calmness. The Center for Humane Technology has started a useful list of resources for this purpose.\nSustainability
\nWhat’s the impact of our work on the world’s environment, resources, and climate? Instead of continuously adding new features in the unrelenting scrum treadmill, how could we design for fewer? We’re in the position to create responsible digital solutions that enable sustainable consumer behavior and prevent overconsumption. For example, apps such as Optimiam and Too Good To Go allow people to order leftover food that would normally be thrashed. Or consider Mutum and Peerby, whose peer-to-peer platforms promote the sharing and reuse of owned products.\nSociety
\nThe Ledger of Harms of the Center for Human Technology is a work-in-progress collection of the negative impacts that digital technology has on society, including topics such as relationships, mental health, and democracy. Designers who are mindful of society consider the impact of their work on the global economy, communities, politics, and health.\n\n[caption id="attachment_7171650" align="alignnone" width="1200"]
The focus areas of design ethics. That’s a lot to consider![/caption]\nEthics as an inconvenience
\nIdeally, in every design project, we should assess the potential impact in all of the above-mentioned areas and take steps to prevent harm. Yet there are many legitimate, understandable reasons why we often neglect to do so. It’s easy to have moral principles, yet in the real world, with the constraints that our daily life imposes upon us, it’s seldom easy to act according to those principles.\nWe might simply say it’s inconvenient at the moment. That there’s a lack of time or budget to consider all the ethical implications of our work. That there are many more pressing concerns that have priority right now. We might genuinely believe it’s just a small issue, something to consider later, perhaps. Mostly, we are simply unaware of the possible consequences of our work.\nAnd then there’s the sheer complexity of it all: it’s simply too much to simultaneously focus on. When short on time, or in the heat of approaching deadlines and impatient stakeholders, how do you incorporate all of design ethics’ focus areas?\nWhere do you even start?\nEthics as a structural practice
\nFor these reasons, I believe we need to elevate design ethics to a more practical level. We need to find ways to make ethics not an afterthought, not something to be considered separately, but rather something that’s so ingrained in our process that not doing it means not doing design at all.\nThe only way to overcome the “inconvenience” of acting ethically is to practice daily ethical design: ethics structurally integrated in our daily work, processes, and tools as designers. No longer will we have to rely on the exceptions among us; those extremely principled who are brave enough to stand up against the system no matter what kind of pressure is put upon them. Because the system will be on our side.\nBy applying ethics daily and structurally in our design process, we’ll be able to identify and neutralize in a very early stage the potential for mistakes and misuse. We’ll increase the quality of our design and our practices simply because we’ll think things through more thoroughly, in a more conscious and structured manner.\nBut perhaps most important is that we’ll establish a new standard for design. A standard that we can sell to our clients as the way design should be done, with ethical design processes and deliverables already included. A standard that can be taught to design students so that the newest generation of designers doesn’t know any better than to apply ethics, always.\nHow to practice daily ethical design?
\nAt this point we’ve arrived at the question of how we can structurally integrate ethics into our design process. How do we make sure that our daily design decisions will result in a product that’s usable and accessible; protects people’s privacy, agency, and focus; and benefits both society and nature?\nI want to share with you some best practices that I’ve identified so far, and how I’ve tried to apply them during a recent project at Mirabeau. The goal of the project was to build a web application that provides a shaver manufacturer’s factory workers insight into the real-time availability of production materials.\nConnect to your organization’s mission and values
\nBy connecting our designs to the mission and values of the companies we work for, we can structurally use our design skills in a strategic manner, for moral purposes. We can challenge the company to truly live up to its promises and support it in carrying out its mission. This does, however, require you to be aware of the company’s values, and to compare these to your personal values.\nAs I had worked with our example client before, I knew it was a company that takes care of its employees and has a strong focus on creating a better world. During the kick-off phase, we used a strategy pyramid to structure the client’s mission and values, and to agree upon success factors for the project. We translated the company’s customer-facing brand guidelines to employee-focused design principles that maintained the essence of the organization.\nKeep track of your assumptions
\nThroughout our entire design process, we make assumptions for each decision that we take. By structurally keeping track of these assumptions, you’ll never forget about the limitations of your design and where the potential risks lie in terms of (harmful) impact on users, the project, the company, and society.\nIn our example project, we listed our assumptions about user goals, content, and functionalities for each page of the application. If we were not fully sure about the value for end users, or the accuracy of a user goal, we marked it as a value assumption. When we were unsure if data could be made available, we marked this as a data (feasibility) assumption. If we were not sure whether a feature would add to the manufacturer’s business, we marked it as a scope assumption. Every week, we tested our assumptions with end users and business stakeholders through user tests and sprint demos. Each design iteration led to new questions and assumptions to be tested the next week.\nAim to be proven wrong
\nWhile our assumptions are the known unknowns, there are always unknown unknowns that we aren’t aware of but could be a huge risk for the quality and impact of our work. The only way we can identify these is by applying the scientific principle of falsifiability: seeking actively to be proven wrong. Only outsiders can point out to us what we miss as an individual or as a team.\nIn our weekly user tests, we included factory workers and stakeholders with different disciplines, from different departments, and working in different contexts, to identify the edge cases that could break our concept. On one occasion, this made us reconsider the entirety of our concept. Still, we could have done better: although scalability to other factories was an important success factor, we were unable to gather input from those other factories during the project. We felt our only option was to mention this as a risk (“limit to scalability”).\nUse the power of checklists
\nLet’s face it: we forget things. (Without scrolling up the page, can you name all the focus areas of design ethics?) This is where checklists help us out: they provide knowledge in the world, so that we don’t have to process it in our easily overwhelmed memory. Simple yet powerful, a checklist is an essential tool to practice daily ethical design.\nIn our example project, we used checklists to maintain an overview of questions and assumptions to user test, checking whether we included our design principles properly, and assessing whether we complied to the client’s values, design principles, and the agreed-upon success factors. In hindsight, we could also have taken a moment during the concept phase to go through the list of focus areas for design ethics, as well as have taken a more structural approach to check accessibility guidelines.\nThe main challenge for daily ethical design
\nMost ethics focus areas are quite tangible, where design decisions have immediate, often visible effects. While certainly challenging in their own right, they’re relatively easy to integrate in our daily practice, especially for experienced designers.\nSociety and the environment, however, are more intangible topics; the effects of our work in these areas are distant and uncertain. I’m sure that when Airbnb was first conceived, the founders did not consider the magnitude of its disruptive impact on the housing market. The same goes for Instagram, as its role in creating demand for fast fashion must have been hard to foresee.\nHard, but not impossible. So how do we overcome this challenge and make the impact that we have on society and the environment more immediate, more daily?\nConduct Dark Reality sessions
\nThe ancient Greek philosopher Socrates used a series of questions to gradually uncover the invalidity of people’s beliefs. In a very similar way, we can uncover the assumptions and potential disastrous consequences of our concepts in a ‘Dark Reality’ session, a form of speculative design that focuses on stress-testing a concept with challenging questions.\nWe have to ask ourselves—or even better, somebody outside our team has to ask us— questions such as, “What is the lifespan of your product? What if the user base will be in the millions? What are the long-term effects on economy, society, and the environment? Who benefits from your design? Who loses? Who is excluded? And perhaps most importantly, how could your design be misused? (For more of these questions, Alan Cooper provided a great list in his keynote at Interaction 18.)\nThe back-and-forth Q&A of the Dark Reality session will help us consider and identify our concept’s weaknesses and potential consequences. As it is a team effort, it will spark discussion and uncover differences in team members’ ethical values. Moreover, the session will result in a list of questions and assumptions that can be tested with potential users and subject matter experts. In the project for the airline control center, it resulted in more consideration for the human role in automatization and how digital interfaces can continue to support human capabilities (instead of replacing them), and reflection on the role of airports in future society.\nThe dark reality session is best conducted during the convergent parts of the double diamond, as these are the design phases in which we narrow down to realistic ideas. It’s vital to have a questioner from outside the team with strong interviewing skills and who doesn’t easily accept an answer as sufficient. There are helpful tools available to help structure the session, such as the Tarot Cards of Tech and these ethical tools.\nTake a step back to go forward
\nAs designers, we’re optimists by nature. We see the world as a set of problems that we can solve systematically and creatively if only we try hard enough. We intend well. However, merely having the intention to do good is not going to be enough. Our mindset comes with the pitfall of (dis)missing potential disastrous consequences, especially under pressure of daily constraints. That’s why we need to regularly, systematically take a step back and consider the future impact of our work. My hope is that the practical, structural mindset to ethics introduced in this article will help us agree on a higher standard for design.
","direction":"ltr"},"alternate":[{"href":"http://feedproxy.google.com/~r/alistapart/main/~3/72UB8D_Bi2U/","type":"text/html"}],"canonical":[{"href":"https://alistapart.com/article/daily-ethical-design/","type":"text/html"}],"crawled":1569829277486,"published":1569829277486,"origin":{"streamId":"feed/http://feeds.feedburner.com/alistapart/main","title":"A List Apart: The Full Feed","htmlUrl":"https://alistapart.com"},"visual":{"url":"http://www.blogcdn.com/www.engadget.com/media/2013/10/nvidia-shield-console-mode.jpg","width":620,"height":340,"contentType":"image/jpg"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/885f2e01-d314-4e63-abac-17dcb063f5b5","label":"Programming"}]},{"id":"Yn8gu9QqU/Nu8FbPliaB+X/354R2g5xNVMORFC3HITI=_16d811bfa6e:1c48:90d684ff","keywords":["Community, Content, Design, Industry & Business, Information Architecture, User Experience"],"originId":"https://alistapart.com/article/trans-inclusive-design/","fingerprint":"27eba67f","title":"Trans-inclusive Design","author":"by Erin White","summary":{"content":"\nLate one night a few years ago, a panicked professor emailed me: “My transgender student’s legal name is showing on our online discussion board. How can I keep him from being outed to his classmates?” Short story: we couldn’t. The professor created an offline workaround with the student. Years later this problem persists not just in campus systems, but in many systems we use every day.
\n\n\nTo anyone who’d call that an unusual situation, it’s not. We are all already designing for trans users—1 in 250 people in the US identifies as transgender or gender non-binary (based on current estimates), and the number is rising.
\n\n\nWe are web professionals; we can do better than an offline workaround. The choices we make impact the online and offline experiences of real people who are trans, non-binary, or gender-variant—choices that can affirm or exclude, uplift or annoy, help or harm.
\n\n\nThe rest of this article assumes you agree with the concept that trans people are human beings who deserve dignity, respect, and care. If you are seeking a primer on trans-related vocabulary and concepts, please read up and come back later.
\n\n\nI’m going to cover issues touching on content, images, forms, databases, IA, privacy, and AI—just enough to get you thinking about the decisions you make every day and some specific ideas to get you started.
\n\n\n“Tried making a Bitmoji again, but I always get disillusioned immediately by their binary gender model from literally step 1 and end up not using it. I don’t feel represented.”
Editorial note: All personal statements quoted in this article have been graciously shared with the express consent of the original authors.
\n\n\nHow we can get things right
\n\n\nGender is expansively misconstrued as some interchangeable term for anatomical features. Unlike the constellation of human biological forms (our sex), gender is culturally constructed and varies depending on where you are in the world. It has its own diversity.
\n\n\nAsking for gender when it is not needed; limiting the gender options users can select; assuming things about users based on gender; or simply excluding folks from our designs are all ways we reify the man-woman gender binary in design decisions.
\n\n\nNames are fundamentally important
\n\n\nIf we do nothing else, we must get names right. Names are the difference between past and present, invalidation and affirmation, and sometimes safety and danger.
\n\n\nYet, many of the systems we use and create don’t offer name flexibility.
\n\n\nMany programmers and designers have a few misconceptions about names, such as assuming people have one moniker that they go by all the time, despite how common it is for names to change over a lifetime. People might update them after a change in marital status, family situation, or gender, or perhaps someone is known by a nickname, westernized name, or variation on a first name.
\n\n\nIn most locales, legally changing names is extremely difficult, extremely expensive, requires medical documentation, or is completely out of the question.
\n\n\nChanges to name and gender marker are even more complicated; they tend to be two separate, long-drawn-out processes. To make matters worse, laws vary from state to state within the U.S. and most only recognize two genders—man and woman—rather than allowing non-binary options.Not all trans people change their names, but for those who do, it’s a serious and significant decision that shouldn’t be sabotaged. We can design systems that protect the lives and privacy of our users, respect the fluid nature of personal identity, and act as an electronic curb cut that helps everyone in the process.
\n\n\nDeadnaming
\n\n\nOne need only search Twitter for “deadname app” to get an idea of how apps can leave users in the lurch. Some of the most alarming examples involve apps and sites that facilitate real-life interactions (which already involve a measure of risk for everyone).
\n\n\n“Lyft made it completely impossible for me to change my name on its app even when it was legally changed. I reached out to their support multiple times and attempted to delete the account and start over with no result. I was completely dependent on this service for groceries, appointments, and work, and was emotionally exhausted every single time I needed a ride. I ended up redownloading Uber - even though there was a strike against the service - which I felt awful doing. But Uber allowed me to change my name without any hoops to jump through, so for the sake of my mental health, I had to.”
\n\n\nTrans people are more likely to experience financial hardship, so using payment apps to ask for donations is often necessary. Some of these services may reveal private information as a matter of course, leaving them exposed and potentially at risk.
\n\n\nThere are also ramifications when linked services rely on our data sources for name information, instigating an unpredictable cascade effect with little or no recourse to prevent the sharing of sensitive details.
\n\n\nThese are examples of deadnaming. Deadnaming is what happens when someone’s previous or birth name is used, rather than the name the person uses now. Deadnaming is invalidating at the least, even as a faux pas, but can be psychologically devastating at the other extreme, even putting lives at risk.The experiences of trans, non-binary, or gender-variant folk can vary widely, and they live in disparate conditions throughout the world. Many are thriving and creating new and joyful ways to resist and undo gender norms, despite the common negative narrative around trans lives. Others can face hardship; trans people are more likely to be unstably housed, underemployed, underpaid, and targets of violence in and out of their homes, workplaces, and intimate relationships. The ramifications are amplified for people of color and those with disabilities, as well as those in precarious living/working situations and environments where exposure can put them in harm’s way.
\n\n\nDesign for name changes
\n\n\nHere’s what we can do:
\n\n\nDesign for renaming. Emma Humphries’ talk on renaming covers this nicely. Airbnb has developed policies and procedures for users who’ve transitioned, allowing users to keep their review histories intact with amended names and/or pronouns.
\n\n\nGet rid of “real name” requirements. Allow people to use names they go by rather than their legal first names.
\n\n\nClarify when you actually need a legal name, and only use that in conjunction with a display name field.
\n\n\nHave a name change process that allows users to change their names without legal documentation. (It’s likely that you have procedures for marriage-related name changes already.)
\n\n\nEnsure users can still change their display names when connecting with other data sources to populate users’ names.
\n\n\nDon’t place onerous restrictions on changes. Once someone creates a username, web address, or profile URL, allow them to change it.
\n\n\nDraft a code of conduct if you’re part of an online community, and make sure to include policies around deadnaming. Twitter banned deadnaming last year.
\n\n\nAllow people to be forgotten. When people delete their accounts for whatever reason, help them make sure that their data is not lingering in your systems or in other places online.
\n\n\nUpdate the systems users don’t see, too
\n\n\nIdentity management systems can be a mess, and name changes can reveal the failures among those systems, including hidden systems that users don’t see.
\n\n\nOne Twitter user’s health insurance company kept their ID number between jobs but changed their gender. Another user updated their display name but got an email confirmation addressed to their legal name.
\n\n\nHidden information can also undermine job opportunities:
\n\n\n“At a university as a student, I transitioned and changed my name and gender to be a woman. TWELVE YEARS later after being hired to work in the Libraries, the Libraries HR coordinator emailed me that I was listed as male still in the database. He changed it on my asking, but I have to wonder how long… was it a factor in my being turned down for jobs I applied to… who had seen that..?”
\n\n\nEmma Humphries details the hidden systems that can carry out-of-date information about users. Her tips for database design include:
\n\n\n- Don’t use emails as unique IDs.
- Use an invariant user ID internally, and link the user’s current email and display name to it.
\n\n\nImages
\n\n\nVisuals should allow room for representation and imagination rather than a narrow subset of the usual suspects: figures who appear to be straight, cisgender, able-bodied, and white/Caucasian.
\n\n\nWhat we can do is feature a variety of gender presentations, as well as not assume someone’s gender identity if they buy certain items.
\n\n\nSome companies, like Wildfang and Thinx, offer a broad array of images representing different races, body sizes, and gender expressions on their websites and in their ads.
\n\n\nMany are also choosing not to hire models, allowing room for imagination and versatility:
\n\n\n“I got a catalog for a ‘classic menswear company’ that features zero photos of any person of any gender. Now if only I could afford an $800 blazer...”
\n\n\nHere's what we can do:
\n\n\nActively recruit diverse groups of models for photos. And pay them!
\n\n\nIf you can’t shoot your own photos, Broadly has recently launched a trans-inclusive stock photo collection free for wide use. Avataaars allows users to create an avatar without selecting a gender.
\n\n\nInformation architecture
\n\n\nHow we organize information is a political act and a non-neutral decision (librarians have said this for a while). This applies to gender-based classifications.
\n\n\nMany companies that sell consumer goods incorporate gender into their product design and marketing, no matter what. The product itself might be inherently gender-neutral (such as clothing, toys, bikes, or even wine), but these design and marketing decisions can directly impact the information architecture of websites.
\n\n\nHere's what we can do:
\n\n\nEvaluate why any menus, categories, or tags are based on gender, and how it can be done differently:
\n\n\n“Nike has a ‘gender neutral’ clothing category, yet it’s listed under ‘men’ and ‘women’ in the website architecture. \uD83E\uDD14”
\n\n\nForms
\n\n\nForms, surveys, and other types of data gathering are surefire ways to include or exclude people. If you ask for information you don’t need or limit the options that people can select, you risk losing them as users.
\n\n\nHere's what we can do:
\n\n\nCritically evaluate why you are asking for personal information, including gender. Will that information be used to help someone, or sell things to your advertisers?
\n\n\n"Why does the @CocaCola site make me select a gender just to make a purchase? Guess my family isn't getting personalized Coke bottles for Christmas."
\n\n\nIf you are asking users for their gender, you’d better have a good reason and options that include everyone. A gender field should have more than two options, or should ask for pronouns instead. When including more than binary options, actually record the selections in your databases instead of reclassifying answers as male/female/null, otherwise you risk losing trust when disingenuous design decisions become public.
\n\n\nHonorifics are infrequently used these days, but it takes little work to add gender-inclusive titles to a list. For English-language sites, “Mx.” can go alongside “Mr.” and “Ms.” without fuss. United Airlines debuted this option earlier this year.
\n\n\nContent
\n\n\nHere's what we can do:
\n\n\nAvoid inappropriately gendered language. Your style guide should include singular “they” instead of “he/she” or “s/he,” and exclude frequently used words and phrases that exclude trans folks. Resources such as this transgender style guide are a quick way to check your language and benchmark your own content guidelines.
\n\n\nCheck assumptions about gender and biology. Not everyone who can have a period, can get pregnant, or can breastfeed identifies as women or mothers—just as not everyone who identifies as women or mothers can have periods, can get pregnant, or can breastfeed. Thinx, a company that sells period underwear, has an inclusive tagline: “For people with periods.”
\n\n\nAvoid reinforcing the binary. Groups of people aren’t “ladies and gentlemen” or “boys and girls.” They are folks, people, colleagues, “y’all,” or even “all y’all.”
\n\n\nPronouns aren’t “preferred”—they’re just pronouns. Calling pronouns preferred suggests that they’re optional and are replacing a “true” pronoun.
\n\n\nAvoid reinforcing stereotypes about trans people. Not all trans people are interested in medically transitioning, or in “passing.” They also aren’t fragile or in need of a savior. Gender is separate from sexual orientation. You can’t “tell” someone is trans.
\n\n\nPrivacy, surveillance, and nefarious AI
\n\n\nWe’ve heard the story of algorithms identifying a pregnant teen before her parents knew. What if an algorithm predicts or reveals information about your gender identity?
\n\n\nInferences. Users’ genders are assumed based on their purchase/browsing history.
\n\n\nRecommendations. A user bought something before they transitioned and it shows up in “recommended because you bought X.”
\n\n\nPredictions. Users’ genders are not only inferred but used to predict something else based on characteristics of that gender. Even if you don’t tell big websites what your gender is, they assume one for you based on your interests. That kind of reductive essentialism can harm people of all genders. One of this article’s peer readers summed this up:
\n\n\n“Gender markers are a poor proxy for tastes. I like dresses, cute flats, and Raspberry Pis.”
\n\n\nFlashbacks. “On this day” algorithms remind users of the past, sometimes for better (“I’ve come so far”) or for worse (“don’t remind me”).
\n\n\nAI-based discrimination
\n\n\nAI and surveillance software can also reinforce norms about what men’s and women’s bodies should look like, resulting in harrowing airline travel experiences and creating AI-based discrimination for trans people.
\n\n\nSo, too, can trans folks’ public data be used for projects that they don’t consent to. Just because we can use AI for something—like determining gender based on a face scan—doesn’t mean we should.
\n\n\nHere's what we can do:
\n\n\nRead up and proactively mitigate bias. AI and algorithms can reflect developers’ biases and perpetuate stereotypes about how people’s bodies should look. Use AI to challenge the gender binary rather than reinforce it. Design for privacy first. Hire more types of people who represent different lived experiences.
\n\n\nToward a gender-inclusive web
\n\n\nThe ideas I’ve offered here are only starting points. How you choose to create space for trans folks is going to be up to you. I don’t have all the solutions here, and there is no singular trans experience. Also, language, definitions, and concepts change rapidly.
\n\n\nWe shouldn’t use any of these facts as excuses to keep us from trying.
\n\n\nWhen we start to think about design impact on trans folks, the ideas we bring into question can benefit everyone. Our designs should go beyond including—they should affirm and validate. Ideally, they will also reflect organizational cultures that support diversity and inclusion.
\n\n\nHere's what we can do:
\n\n\nKeep learning. Learn how to be a good ally. Pay trans user research participants to help validate your design assumptions. Hire trans people on your team and don't hang them out to dry or make them do all the hard work around inclusion and equity. Make it everyone’s job to build a more just web and world for everybody.
\n\n\nEditorial note: All personal statements quoted in this article have been graciously shared with the express consent of the original authors.
\n\n\nThis article is stronger and wiser thanks to Mica McPheeters at A List Apart and the following peer readers. Thank you.
\n\n\nJake Atchison
Katherine Deibel, Ph.D.
Justina F. Hall
Austyn Higgs
Emma Humphries
Tara Robertson
Levi R. Walter
\n
","direction":"ltr"},"alternate":[{"href":"http://feedproxy.google.com/~r/alistapart/main/~3/h3VJ6VzV3vU/","type":"text/html"}],"canonical":[{"href":"https://alistapart.com/article/trans-inclusive-design/","type":"text/html"}],"crawled":1569829157486,"published":1569829157486,"origin":{"streamId":"feed/http://feeds.feedburner.com/alistapart/main","title":"A List Apart: The Full Feed","htmlUrl":"https://alistapart.com"},"visual":{"url":"http://www.blogcdn.com/www.engadget.com/media/2013/10/nvidia-shield-console-mode.jpg","width":620,"height":340,"contentType":"image/jpg"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/885f2e01-d314-4e63-abac-17dcb063f5b5","label":"Programming"}]},{"id":"Yn8gu9QqU/Nu8FbPliaB+X/354R2g5xNVMORFC3HITI=_16d811a25ae:1c47:90d684ff","keywords":["Information Architecture"],"originId":"https://alistapart.com/article/everyday-information-architecture-excerpt/","fingerprint":"e72e3567","title":"Everyday Information Architecture: Auditing for Structure","author":"by Lisa Maria Martin","summary":{"content":"\nJust as we need to\nunderstand our content before we can recategorize it, we need to understand the\nsystem before we try to rebuild it.
\n\n\nEnter the structural audit: a review of the site focused solely on its menus, links, flows, and hierarchies. I know you thought we were done with audits back in Chapter 2, but hear me out! Structural audits have an important and singular purpose: to help us build a new sitemap.
\n\n\nThis isn’t about recreating the intended sitemap—no, this is about experiencing the site the way users experience it. This audit is meant to track and record the structure of the site as it really works.
\n\n\nSetting up the template
\n\n\nFirst, we’re gonna need another spreadsheet. (Look, it is not my fault that spreadsheets are the perfect system for recording audit data. I don’t make the rules.)
\n\n\nBecause this involves building a spreadsheet from scratch, I keep a “template” at the top of my audit files—rows that I can copy and paste into each new audit (Fig 4.1). It’s a color-coded outline key that helps me track my page hierarchy and my place in the auditing process. When auditing thousands of pages, it’s easy to get dizzyingly lost, particularly when coming back into the sheet after a break; the key helps me stay oriented, no matter how deep the rabbit hole.
\n\n\n
Fig 4.1: I use a color-coded outline key to record page hierarchy as I move through the audit. Wait, how many circles did Dante write about? \n\n\nColor-coding
\n\n\nColor is the easiest, quickest way to convey page depth at a\nglance. The repetition of black text, white cells, and gray lines can have a\nnumbing effect—too many rows of sameness, and your eyes glaze over. My coloring\nmay result in a spreadsheet that looks like a twee box of macarons, but at\nleast I know, instantly, where I am.
\n\n\nThe exact colors don’t really matter, but I find that the\nfamiliar mental model of a rainbow helps with recognition—the cooler the row color,\nthe deeper into the site I know I must be.
\n\n\nThe nested rainbow of pages is great when you’re auditing neatly nested pages—but most websites color outside the lines (pun extremely intended) with their structure. I leave my orderly rainbow behind to capture duplicate pages, circular links, external navigation, and other inconsistencies like:
\n\n\n- On-page navigation. A bright text color denotes pages that are accessible via links within page content—not through the navigation. These pages are critical to site structure but are easily overlooked. Not every page needs to be displayed in the navigation menus, of course—news articles are a perfect example—but sometimes this indicates publishing errors.
- External links. These are navigation links that go to pages outside the domain. They might be social media pages, or even sites held by the same company—but if the domain isn’t the one I’m auditing, I don’t need to follow it. I do need to note its existence in my spreadsheet, so I color the text as the red flag that it is. (As a general rule, I steer clients away from placing external links in navigation, in order to maintain a consistent experience. If there’s a need to send users offsite, I’ll suggest using a contextual, on-page link.)
- Files. This mostly refers to PDFs, but can include Word files, slide decks, or anything else that requires downloading. As with external links, I want to capture anything that might disrupt the in-site browsing experience. (My audits usually filter out PDFs, but for organizations that overuse them, I’ll audit them separately to show how much “website” content is locked inside.)
- Unknown hierarchy. Every once in a while, there’s a page that doesn’t seem to belong anywhere—maybe it’s missing from the menu, while its URL suggests it belongs in one section and its navigation scheme suggests another. These pages need to be discussed with their owners to determine whether the content needs to be considered in the new site.
- Crosslinks. These are navigation links for pages that canonically live in a different section of the site—in other words, they’re duplicates. This often happens in footer navigation, which may repeat the main navigation or surface links to deeper-but-important pages (like a Contact page or a privacy policy). I don’t want to record the same information about the page twice, but I do need to know where the crosslink is, so I can track different paths to the content. I color these cells gray so they don’t draw my attention.
\n\n\nNote that coloring every row (and indenting, as you’ll see in a moment) can be a tedious process—unless you rely on Excel’s formatting brush. That tool applies all the right styles in just two quick clicks.
\n\n\nOutlines and page IDs
\n\n\nColor-coding is half of my template; the other half is the outline, which is how I keep track of the structure itself. (No big deal, just the entire point of the spreadsheet.)
\n\n\nEvery page in the site gets assigned an ID. You are assigning this number; it doesn’t correspond to anything but your own perception of the navigation. This number does three things for you:
\n\n\n- It associates pages with their place in the site hierarchy. Decimals indicate levels, so the page ID can be decoded as the page’s place in the system.
- It gives each page a unique identifier, so you can easily refer to a particular page—saying “2.4.1” is much clearer than “you know that one page in the fourth product category?”
- You can keep using the ID in other contexts, like your sitemap. Then, later, when your team decides to wireframe pages 1.1.1 and 7.0, you’ll all be working from the same understanding.
\n\n\nLet me be completely honest: things might get goofy sometimes\nwith the decimal outline. There will come a day when you’ll find yourself\ncasually typing out “1.2.1.2.1.1.1,” and at that moment, a fellow auditor somewhere\nin the universe will ring a tiny gong for you.
\n\n\nIn addition to the IDs, I indent each level, which reinforces\nboth the numbers and the colors. Each level down—each digit in the ID, each\nchange in color—gets one indentation.
\n\n\nI identify top-level pages with a single number: 1.0, 2.0, 3.0, etc. The next page level in the first section would be 1.1, 1.2, 1.3, and so on. I mark the homepage as 0.0, which is mildly controversial—the homepage is technically a level above—but, look: I’ve got a lot of numbers to write, and I don’t need those numbers to tell me they’re under the homepage, so this is my system. Feel free to use the numbering system that work best for you.
\n\n\nCriteria and columns
\n\n\nSo we’ve got some secret codes for tracking hierarchy and depth, but what about other structural criteria? What are our spreadsheet columns (Fig 4.2)? In addition to a column for Page ID, here’s what I cover:
\n\n\n- URL.\nI don’t consistently fill out this column, because I already collected this\ndata back in my automated audit. I include it every twenty entries or so (and\non crosslinks or pages with unknown hierarchy) as another way of tracking\nprogress, and as a direct link into the site itself.
- Menu\nlabel/link. I include this column only if I notice a lot of\nmismatches between links, labels, and page names. Perfect agreement isn’t\nrequired; but frequent, significant differences between the language that leads\nto a page and the language on the page itself may indicate\ninconsistencies in editorial approach or backend structures.
- Name/headline.\nThink of this as “what does the page owner call it?” It may be\nthe H1, or an H2; it may match the link that brought you here, or the page\ntitle in the browser, or it may not.
- Page\ntitle. This is for the name of the page in the metadata. Again,\nI don’t use this in every audit—particularly if the site uses the same long,\nbranded metadata title for every single page—but frequent mismatches can be\nuseful to track.
- Section.\nWhile the template can indicate your level, it can’t tell you which area of the\nsite you’re in—unless you write it down. (This may differ from the section data\nyou applied to your automated audit, taken from the URL structure; here, you’re\nnoting the section where the page appears.)
- Notes.\nFinally, I keep a column to note specific challenges, and to track patterns I’m\nseeing across multiple pages—things like “Different template, missing subnav”\nor “Only visible from previous page.” My only caution here is that if you’re planning\nto share this audit with another person, make sure your notes are—ahem—professional.\nUnless you enjoy anxiously combing through hundreds of entries to revise comments\nlike “Wow haha nope” (not that I would know anything about that).
\n\n\n
Fig 4.2: A semi-complete structural audit. This view shows a lot of second- and third-level pages, as well as pages accessed through on-page navigation.\n\n\nDepending on your project needs, there may be other columns, too.\nIf, in addition to using this spreadsheet for your new sitemap, you want to use\nit in migration planning or template mapping, you may want columns for new\nURLs, or template types.
\n\n\nYou can get your own copy of my template as a downloadable Excel file. Feel free to tweak it to suit your style and needs; I know I always do. As long as your spreadsheet helps you understand the hierarchy and structure of your website, you’re good to go.
\n\n\nGathering data
\n\n\n Setting up the template is one thing—actually filling it out is, admittedly, another. So how do we go from a shiny, new, naive spreadsheet to a complete, jaded, seen-some-stuff spreadsheet? I always liked Erin Kissane’s description of the process, from The Elements of Content Strategy:
\n\n\nBig inventories involve a lot of black coffee, a few late nights, and a playlist of questionable but cheering music prominently featuring the soundtrack of object-collecting video game Katamari Damacy. It takes quite a while to exhaustively inventory a large site, but it’s the only way to really understand what you have to work with.
\n\n\nWe’re not talking about the same kind of exhaustive inventory she\nwas describing (though I am recommending Katamari music). But even our\nless intensive approach is going to require your butt in a seat, your eyes on a\nscreen, and a certain amount of patience and focus. You’re about to walk, with\nyour fingers, through most of a website.
\n\n\nStart on the homepage. (We know that not all users start there,\nbut we’ve got to have some kind of order to this process or we’ll never get\nthrough it.) Explore the main navigation before moving on to secondary\nnavigation structures. Move left to right, top to bottom (assuming that is your\nlanguage direction) over each page, looking for the links. You want to record\nevery page you can reasonably access on the site, noting navigational and\nstructural considerations as you go.
\n\n\nMy advice as you work:
\n\n\n- Use two monitors. I struggle immensely without two screens in this process, which involves constantly switching between spreadsheet and browser in rapid, tennis-match-like succession. If you don’t have access to multiple monitors, find whatever way is easiest for you to quickly flip between applications.
- Record what you see. I generally note all visible menu links at the same level, then exhaust one section at a time. Sometimes this means I have to adjust what I initially observed, or backtrack to pages I missed earlier. You might prefer to record all data across a level before going deeper, and that would work, too. Just be consistent to minimize missed links.
- Be alert to inconsistencies. On-page links, external links, and crosslinks can tell you a lot about the structure of the site, but they’re easy to overlook. Missed on-page links mean missed content; missed crosslinks mean duplicate work. (Note: the further you get into the site, the more you’ll start seeing crosslinks, given all the pages you’ve already recorded.)
- Stick to what’s structurally relevant. A single file that’s not part of a larger pattern of file use is not going to change your understanding of the structure. Neither is recording every single blog post, quarterly newsletter, or news story in the archive. For content that’s dynamic, repeatable, and plentiful, I use an x in the page ID to denote more of the same. For example, a news archive with a page ID of 2.8 might show just one entry beneath it as 2.8.x; I don’t need to record every page up to 2.8.791 to understand that there are 791 articles on the site (assuming I noted that fact in an earlier content review).
- Save. Save frequently. I cannot even begin to speak of the unfathomable heartbreak that is Microsoft Excel burning an unsaved audit to the ground.
\n\n\nKnowing which links to follow, which to record, and how best to\nuntangle structural confusion—that improves with time and experience. Performing\nstructural audits will not only teach you about your current site, but will\nhelp you develop fluency in systems thinking—a boon when it comes time to\ndocument the new site.
\n
","direction":"ltr"},"alternate":[{"href":"http://feedproxy.google.com/~r/alistapart/main/~3/Ls-z54zDDFU/","type":"text/html"}],"canonical":[{"href":"https://alistapart.com/article/everyday-information-architecture-excerpt/","type":"text/html"}],"crawled":1569829037486,"published":1569829037486,"origin":{"streamId":"feed/http://feeds.feedburner.com/alistapart/main","title":"A List Apart: The Full Feed","htmlUrl":"https://alistapart.com"},"visual":{"url":"http://www.blogcdn.com/www.engadget.com/media/2013/10/nvidia-shield-console-mode.jpg","width":620,"height":340,"contentType":"image/jpg"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/885f2e01-d314-4e63-abac-17dcb063f5b5","label":"Programming"}]},{"id":"Yn8gu9QqU/Nu8FbPliaB+X/354R2g5xNVMORFC3HITI=_16d811850ee:1c46:90d684ff","keywords":["Community, Industry"],"originId":"https://alistapart.com/article/nothing-fails-like-success/","fingerprint":"9a31d3f6","title":"Nothing Fails Like Success","author":"by Jeffrey Zeldman","summary":{"content":"\nA family buys a house they can’t afford. They can’t make their monthly mortgage payments, so they borrow money from the Mob. Now they’re in debt to the bank and the Mob, live in fear of losing their home, and must do whatever their creditors tell them to do.
\n\n\nWelcome to the internet, 2019.
\n\n\nBuying something you can’t afford, and borrowing from organizations that don’t have your (or your customers’) best interest at heart, is the business plan of most internet startups. It’s why our digital services and social networks in 2019 are a garbage fire of lies, distortions, hate speech, tribalism, privacy violations, snake oil, dangerous idiocy, deflected responsibility, and whole new categories of unpunished ethical breaches and crimes.
\n\n\nFrom optimistically conceived origins and message statements about making the world a better place, too many websites and startups have become the leading edge of bias and trauma, especially for marginalized and at-risk groups.
\n\n\nWhy (almost) everything sucks
\n\n\nTwitter, for instance, needs a lot of views for advertising to pay at the massive scale its investors demand. A lot of views means you can’t be too picky about what people share. If it’s misogynists or racists inspiring others who share their heinous beliefs to bring back the 1930s, hey, it’s measurable. If a powerful elected official’s out-of-control tweeting reduces churn and increases views, not only can you pay your investors, you can even take home a bonus. Maybe it can pay for that next meditation retreat.
\n\n\nYou can cloak this basic economic trade-off in fifty layers of bullshit—say you believe in freedom of speech, or that the antidote to bad speech is more speech—but the fact is, hate speech is profitable. It’s killing our society and our planet, but it’s profitable. And the remaining makers of Twitter—the ones whose consciences didn’t send them packing years ago—no longer have a choice. The guy from the Mob is on his way over, and the vig is due.
\n\n\nNot to single out Twitter, but this is clearly the root cause of its seeming indifference to the destruction hate speech is doing to society…and will ultimately do to the platform. (But by then Jack will be able to afford to meditate full-time.)
\n\n\nOther companies do other evil things to pay their vig. When you owe the Mob, you have no choice. Like sell our data. Or lie about medical research.
\n\n\nThere are internet companies (like Basecamp, or like Automattic, makers of WordPress.com, where I work) that charge money for their products and services, and use that money to grow their business. I wish more internet companies could follow that model, but it’s hard to retrofit a legitimate business model to a product that started its life as free.
\n\n\nAnd there are even some high-end news publications, such as The New York Times, The Washington Post, and The Guardian, that survive on a combination of advertising and flexible paywalls. But these options are not available to most digital publications and businesses.
\n\n\nReturn with me to those Halcyon days…
\n\n\nWebsites and internet startups used to be you and your friends making cool stuff for your other friends, and maybe building new friendships and even small communities in the process. (Even in 2019, that’s still how some websites and startups begin—as labors of love, fashioned by idealists in their spare time.)
\n\n\nBecause they are labors of love; because we’ve spent 25 years training people to believe that websites, and news, and apps, and services should be free; because, when we begin a project, we can scarcely believe anyone will ever notice or care about it—for these reasons and more, the things we make digitally, especially on the web, are offered free of charge. We labor on, excited by positive feedback, and delighted to discover that, if we keep at it, our little community will grow.
\n\n\nMost such labors of love disappear after a year or two, as the creators drift out of touch with each other, get “real” jobs, fall in love, start families, or simply lose interest due to lack of attention from the public or the frustrations of spending weekends and holidays grinding away at an underappreciated site or app while their non-internet friends spend those same hours either having fun or earning money.
\n\n\nAlong came money
\n\n\nBut some of these startup projects catch on. And when they do, a certain class of investor smells ROI. And the naive cofounders, who never expected their product or service to really get anywhere, can suddenly envision themselves rich and Zuckerberg-famous. Or maybe they like the idea of quitting their day job, believing in themselves, and really going for it. After all, that is an empowering and righteous vision.
\n\n\nMaybe they believe that by taking the initial investment, they can do more good—that their product, if developed further, can actually help people. This is often the motivation behind agreeing to an initial investment deal, especially in categories like healthcare.
\n\n\nOr maybe the founders are problem solvers. Existing products or services in a given category have a big weakness. The problem solvers are sure that their idea is better. With enough capital, and a slightly bigger team, they can show the world how to do it right. Most inventions that have moved humankind forward followed exactly this path. It should lead to a better world (and it sometimes does). It shouldn’t produce privacy breaches and fake medicine and election-influencing bots and all the other plagues of our emerging digital civilization. So why does it?
\n\n\nContent wants to be paid
\n\n\nPrimarily it is because these businesses have no business model. They were made and given away free. Now investors come along who can pay the founders, buy them an office, give them the money to staff up, and even help with PR and advertising to help them grow faster.
\n\n\nNow there are salaries and insurance and taxes and office space and travel and lecture tours and sales booths at SXSW, but there is still no charge for the product.
\n\n\nAnd the investor seeks a big return.
\n\n\nAnd when the initial investment is no longer enough to get the free-product company to scale to the big leagues, that’s when the really big investors come in with the really big bucks. And the company is suddenly famous overnight, and “everybody” is using the product, and it’s still free, and the investors are still expecting a giant payday.
\n\n\nLike I said—a house you can’t afford, so you go into debt to the bank and the Mob.
\n\n\nThe money trap
\n\n\nHere it would be easy to blame capitalism, or at least untrammeled, under-regulated capitalism, which has often been a source of human suffering—not that capitalism, properly regulated, can’t also be a force for innovation which ameliorates suffering. That’s the dilemma for our society, and where you come down on free markets versus governmental regulation of businesses should be an intellectual decision, but these days it is a label, and we hate our neighbors for coming down a few degrees to the left or right of us. But I digress and oversimplify, and this isn’t a complaint about late stage capitalism per se, although it may smell like one.
\n\n\nNo, the reason small companies created by idealists too frequently turn into consumer-defrauding forces for evil has to do with the amount of profit each new phase of investor expects to receive, and how quickly they expect to receive it, and the fact that the products and services are still free. And you know what they say about free products.
\n\n\nNothing fails like success
\n\n\nA friend who’s a serial entrepreneur has started maybe a dozen internet businesses over the span of his career. They’ve all met a need in the marketplace. As a consequence, they’ve all found customers, and they’ve all made a profit. Yet his investors are rarely happy.
\n\n\n“Most of my startups have the decency to fail in the first year,” one investor told him. My friend’s business was taking in several million dollars a year and was slowly growing in staff and customers. It was profitable. Just not obscenely so.
\n\n\nAnd internet investors don’t want a modest return on their investment. They want an obscene profit right away, or a brutal loss, which they can write off their taxes. Making them a hundred million for the ten million they lent you is good. Losing their ten million is also good—they pay a lower tax bill that way, or they use the loss to fold a company, or they make a profit on the furniture while writing off the business as a loss…whatever rich people can legally do under our tax system, which is quite a lot.
\n\n\nWhat these folks don’t want is to lend you ten million dollars and get twelve million back.
\n\n\nYou and I might go, “Wow! I just made two million dollars just for being privileged enough to have money to lend somebody else.” And that’s why you and I will never have ten million dollars to lend anybody. Because we would be grateful for it. And we would see a free two million dollars as a life-changing gift from God. But investors don’t think this way.
\n\n\nWe didn’t start the fire, but we roasted our weenies in it
\n\n\nAs much as we pretend to be a religious nation, our society worships these investors and their profits, worships companies that turn these profits, worships above all the myth of overnight success, which we use to motivate the hundreds of thousands of workers who will work nights and weekends for the owners in hopes of cashing in when the stock goes big.
\n\n\nMost times, even if the stock does go big, the owner has found a way to devalue it by the time it does. Owners have brilliant advisers they pay to figure out how to do those things. You and I don’t.
\n\n\nA Christmas memory
\n\n\nI remember visiting San Francisco years ago and scoring an invitation to Twitter’s Christmas party through a friend who worked there at the time. Twitter was, at the time, an app that worked via SMS and also via a website. Period.
\n\n\nSome third-party companies, starting with my friends at Iconfactory, had built iPhone apps for people who wanted to navigate Twitter via their newfangled iPhones instead of the web. Twitter itself hadn’t publicly addressed mobile and might not even have been thinking about it.
\n\n\nAlthough Twitter was transitioning from a fun cult thing—used by bloggers who attended SXSW Interactive in 2007—to an emerging cultural phenomenon, it was still quite basic in its interface and limited in its abilities. Which was not a bad thing. There is art in constraint, value in doing one thing well. As an outsider, if I’d thought about it, I would have guessed that Twitter’s entire team consisted of no more than 10 or 12 wild-eyed, sleep-deprived true believers.
\n\n\nImagine my surprise, then, when I showed up at the Christmas party and discovered I’d be sharing dinner with hundreds of designers, developers, salespeople, and executives instead of the handful I’d naively anticipated meeting. (By now, of course, Twitter employs many thousands. It’s still not clear to an outsider why so many workers are needed.)
\n\n\nBut one thing is clear: somebody has to pay for it all.
\n\n\nFreemium isn’t free
\n\n\nEmployees, let alone thousands of them, on inflated Silicon Valley engineer salaries, aren’t free. Health insurance and parking and meals and HR and travel and expense accounts and meetups and software and hardware and office space and amenities aren’t free. Paying for all that while striving to repay investors tenfold means making a buck any way you can.
\n\n\nSince the product was born free and a paywall isn’t feasible, Twitter must rely on that old standby: advertising. Advertising may not generate enough revenue to keep your hometown newspaper (or most podcasts and content sites) in business, but at Twitter’s scale, it pays.
\n\n\nIt pays because Twitter has so many active users. And what keeps those users coming back? Too often, it’s the dopamine of relentless tribalism—folks whose political beliefs match and reinforce mine in a constant unwinnable war of words with folks whose beliefs differ.
\n\n\nOf course, half the antagonists in a given brawl may be bots, paid for in secret by an organization that wants to make it appear that most citizens are against Net Neutrality, or that most Americans oppose even the most basic gun laws, or that our elected officials work for lizard people. The whole system is broken and dangerous, but it’s also addictive, and we can’t look away. From our naive belief that content wants to be free, and our inability to create businesses that pay for themselves, we are turning our era’s greatest inventions into engines of doom and despair.
\n\n\nYour turn
\n\n\nSo here we are. Now what do we do about it?
\n\n\nIt’s too late for current internet businesses (victims of their own success) that are mortgaged to the hilt in investor gelt. But could the next generation of internet startups learn from older, stable companies like Basecamp, and design products that pay for themselves via customer income—products that profit slowly and sustainably, allowing them to scale up in a similarly slow, sustainable fashion?
\n\n\nThe self-payment model may not work for apps and sites that are designed as modest amusements or communities, but maybe those kinds of startups don’t need to make a buck—maybe they can simply be labors of love, like the websites we loved in the 1990s and early 2000s.
\n\n\nAlong those same lines, can the IndieWeb, and products of IndieWeb thinking like Micro.blog, save us? Might they at least provide an alternative to the toxic aspects of our current social web, and restore the ownership of our data and content? And before you answer, RTFM.
\n\n\nOn an individual and small collective basis, the IndieWeb already works. But does an IndieWeb approach scale to the general public? If it doesn’t scale yet, can we, who envision and design and build, create a new generation of tools that will help give birth to a flourishing, independent web? One that is as accessible to ordinary internet users as Twitter and Facebook and Instagram? Tantek Çelik thinks so, and he’s been right about the web for nearly 30 years. (For more about what Tantek thinks, listen to our conversation in Episode № 186 of The Big Web Show.)
Are these approaches mere whistling against a hurricane? Are most web and internet users content with how things are? What do you think? Share your thoughts on your personal website (dust yours off!) or (irony ahoy!) on your indie or mainstream social networks of choice using hashtag #LetsFixThis. I can’t wait to see what you have to say.
\n
","direction":"ltr"},"alternate":[{"href":"http://feedproxy.google.com/~r/alistapart/main/~3/11S-0ER8qtM/","type":"text/html"}],"canonical":[{"href":"https://alistapart.com/article/nothing-fails-like-success/","type":"text/html"}],"crawled":1569828917486,"published":1569828917486,"origin":{"streamId":"feed/http://feeds.feedburner.com/alistapart/main","title":"A List Apart: The Full Feed","htmlUrl":"https://alistapart.com"},"visual":{"url":"http://www.blogcdn.com/www.engadget.com/media/2013/10/nvidia-shield-console-mode.jpg","width":620,"height":340,"contentType":"image/jpg"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/885f2e01-d314-4e63-abac-17dcb063f5b5","label":"Programming"}]},{"id":"Yn8gu9QqU/Nu8FbPliaB+X/354R2g5xNVMORFC3HITI=_16d81167c2e:1c45:90d684ff","keywords":["Accessibility, User Experience"],"originId":"https://alistapart.com/article/accessibility-for-vestibular/","fingerprint":"4e4d4d7e","title":"Accessibility for Vestibular Disorders: How My Temporary Disability Changed My Perspective","author":"by Facundo Corradini","summary":{"content":"\nAccessibility can be tricky. There are plenty of conditions to take into consideration, and many technical limitations and weird exceptions that make it quite hard to master for most designers and developers.
\n\n\nI never considered myself an accessibility expert, but I took great pride in making my projects Web Content Accessibility Guidelines (WCAG) compliant…ish. They would pass most automated tests, show perfectly in the accessibility tree, and work quite well with keyboard navigation. I would even try (and fail) to use a screen reader every now and then.
\n\n\nBut life would give me a lesson I would probably never learn otherwise: last October, my abled life took a drastic change—I started to feel extremely dizzy, with a constant sensation of falling or spinning to the right. I was suffering from a bad case of vertigo caused by labyrinthitis that made it impossible to get anything done.
\n\n\nVertigo can have a wide range of causes, the most common being a viral infection or tiny calcium crystal free floating in the inner ear, which is pretty much our body’s accelerometer. Any disruption in there sends the brain confusing signals about the body’s position, which causes really heavy nausea, dizziness, and headaches. If you’ve ever felt seasick, it’s quite a similar vibe. If not, think about that feeling when you just get off a rollercoaster…it’s like that, only all day long.
\n\n\nFor most people, vertigo is something they’ll suffer just once in a lifetime, and it normally goes away in a week or two. Incidence is really high, with some estimates claiming that up to 40% of the population suffers vertigo at least once in their lifetime. Some people live all their lives with it (or with similar symptoms caused by a range of diseases and syndromes grouped under the umbrella term of vestibular disorders), with 4% of US adults reporting chronic problems with balance, and an additional 1.1% reporting chronic dizziness, according to the American Speech-Language-Hearing Association.
\n\n\nIn my case, it was a little over a month. Here’s what I learned while going through it.
\n\n\nSlants can trigger vestibular symptoms
\n\n\nIt all started as I was out for my daily jog. I felt slightly dizzy, then suddenly my vision got totally distorted. Everything appeared further away, like looking at a fun house’s distortion mirror. I stumbled back home and rested; at that moment I believed I might have over-exercised, and that hydration, food, and rest were all I needed. Time would prove me wrong.
\n\n\nWhat I later learned was that experiencing vertigo is a constant war between one of your inner ears telling the brain “everything is fine, we’re level and still” and the other ear shouting “oh my God, we’re falling, we’re falling!!!” Visual stimuli can act as an intermediary, supporting one ear’s message or the other’s. Vertigo can also work in the opposite way, with the dizziness interfering with your vision.
\n\n\nI quickly found that when symptoms peaked, staring at a distant object would ease the falling sensation somewhat.
\n\n\nIn the same fashion, some visual stimuli would worsen it.
\n\n\nVertical slants were a big offender in that sense. For instance, looking at a subtle vertical slant (the kind that you’d have to look at twice to make sure it’s not perfectly vertical) on a webpage would instantly trigger symptoms for me. Whether it was a page-long slant used to create some interest beside text or a tiny decoration to mark active tabs, looking at anything with slight slants would instantly send me into the rollercoaster.
\n\n\nHorizontal slants (whatever the degree) and harder vertical slants wouldn’t cause these issues.
\n\n\nMy best guess is that slight vertical slants can look like forced perspective and therefore reinforce the falling-from-height sensation, so I would recommend avoiding vertical slants if you can, or make them super obvious. A slight slant looks like perspective, a harder one looks like a triangle.
\n\n\nTarget size matters (even on mouse-assisted devices)
\n\n\nAfter a magnetic resonance imaging (MRI) scan, some tests to discard neurological conditions, and other treatments that proved ineffective, I was prescribed Cinnarizine.
\n\n\nCinnarizine is a calcium channel blocker—to put it simply, it prevents the malfunctioning inner ear “accelerometer” from sending incorrect info to the brain.
And it worked wonders. After ten days of being barely able to get out of bed, I was finally getting something closer to my normal life. I would still feel dizzy all the time, with some peaks throughout the day, but for the most part, it was much easier.
\n\n\nAt this point, I was finally able to use the computer (but still unable to produce any code at all). To make the best of it, I set on a mission to self-experiment on accessibility for vestibular disorders. In testing, I found that one of the first things that struck me was that I would always miss targets (links and buttons).
\n\n\nI’m from the generation that grew up with desktop computers, so using a mouse is second nature. The pointer is pretty much an extension of my mind, as it is for many who use it regularly. But while Cinnarizine helped with the dizziness, it has a common side effect of negatively impacting coordination and fine motor skills (it is recommended not to drive or operate machinery while under treatment). It was not a surprise when I realized it would be much harder to get the pointer to do what I intended.
\n\n\nThe common behavior would be: moving the pointer past the link I intended to click, clicking before reaching it at all, or having to try multiple times to click on smaller targets.
\n\n\nSuccess Criterion 2.5.5 Target Size (Level AAA) of the World Wide Web Consortium (W3C)’s WCAG recommends bigger target sizes so users can activate them easily. The obvious reason for this is that it’s harder to pinpoint targets on smaller screens with coarser inputs (i.e., touchscreens of mobile devices). A fairly common practice for developers is to set bigger target sizes for smaller viewport widths (assuming that control challenges are only touch-related), while neglecting the issue on big screens expected to be used with mouse input. I know I’m guilty of that myself.
\n\n\nInstead of targeting this behavior for just smaller screen sizes, there are plenty of reasons to create larger target sizes on all devices: it will benefit users with limited vision (when text is scaled up accordingly and colors are of sufficient contrast), users with mobility impairments such as hand tremors, and of course, users with difficulty with fine motor skills.
\n\n\nFont size and spacing
\n\n\nEven while “enjoying” the ease of symptoms provided by the treatment, reading anything still proved to be a challenge for the following three weeks.
\n\n\nI was completely unable to use mobile devices while suffering vertigo due to the smaller font sizes and spacing, so I was forced to use my desktop computer for everything.
\n\n\nI can say I was experiencing something similar to users with mild forms of dyslexia or attention disorders: whenever I got to a website that didn’t follow good font styling, I would find myself reading the same line over and over again.
\n\n\nThis proves once again that accessibility is intersectional: when we improve things for a particular purpose it usually benefits users with other challenges as well. I used to believe recommendations on font styles were mostly intended for the nearsighted and those who have dyslexia. Turns out they are also critical for those with vertigo, and even for those with some cognitive differences. At the end of the day, everybody benefits from better readability.
\n\n\nSome actions you can take to improve readability are:
\n\n\n- Keep line height to at least 1.5 times the font size (i.e.,
line-height: 1.5). - Set the spacing between paragraphs to at least 2.0 times the font size. We can do this by adjusting the margins using relative units such as
em. - Letter spacing should be at least 0.12 times the font size. We can adjust this by using the
letter-spacing CSS property, perhaps setting it in a relative unit. - Make sure to have good contrast between text and its background.
- Keep
font-weight at a reasonable level for the given font-family. Some fonts have thin strokes that make them harder to read. When using thinner fonts, try to improve contrast and font size accordingly, even more than what WCAG would suggest. - Choose fonts that are easy to read. There has been a large and still inconclusive debate on which font styles are better for users, but one thing I can say for sure is that popular fonts (as in fonts that the user might be already familiar with) are generally the least challenging for users with reading issues.
\n\n\nWCAG recommendations on text are fairly clear and fortunately are the most commonly implemented of recommendations, but even they can still fall short sometimes. So, better to follow specific guides on accessible text and your best judgement. Passing automated tests does not guarantee actual accessibility.
\n\n\nAnother issue on which my experience with vertigo proved to be similar to that of people with dyslexia and attention disorders was how hard it was for me to keep my attention in just one place. In that sense…
\n\n\nAnimations are bad (and parallax is pure evil)
\n\n\nVal Head has already covered visually-triggered vestibular disorders in an outstanding article, so I would recommend giving it a good read if you haven’t already.
\n\n\nTo summarize, animations can trigger nausea, dizziness, and headaches in some users, so we should use them purposely and responsibly.
\n\n\nWhile most animations did not trigger my symptoms, parallax scrolling did. I’d never been a fan of parallax to begin with, as I found it confusing. And when you’re experiencing vertigo, the issues introduced by parallax scrolling compound.
\n\n\nReally, there are no words to describe just how bad a simple parallax effect, scrolljacking, or even background-attachment: fixed would make me feel. I would rather jump on one of those 20-G centrifuges astronauts use than look at a website with parallax scrolling.
\n\n\nEvery time I encountered it, I would put the bucket beside me to good use and be forced to lie in bed for hours as I felt the room spinning around me, and no meds could get me out of it. It was THAT bad.
\n\n\nThough normal animations did not trigger a reaction as severe, they still posed a big problem. The extreme, conscious, focused effort it took to read would make it such that anything moving on the screen would instantly break my focus, and force me to start the paragraph all over. And I mean anything.
\n\n\nI would constantly find myself reading a website only to have the typical collapsing navigation bar on scroll distract me just enough that I’d totally lose count of where I was at. Autoplaying carousels were so annoying I would delete them using dev tools as soon as they showed up. Background videos would make me get out of the website desperately.
\n\n\nOver time I started using mouse selection as a pointer; a visual indication of what I’d already read so I could get back to it whenever something distracted me. Then I tried custom stylesheets to disable transforms and animations whenever possible, but that also meant many websites having critical elements not appear at all, as they were implemented to start off-screen or otherwise invisible, and show up on scroll.
\n\n\nOf course, deleting stuff via dev tools or using custom stylesheets is not something we can expect 99.99% of our users to even know about.
\n\n\nSo if anything, consider reducing animations to a minimum. Provide users with controls to turn off non-essential animations (WCAG 2.2.3 Animation from Interactions) and to pause, stop, or hide them (WCAG 2.2.2 Pause, Stop, Hide). Implement animations and transitions in such a way that if the user disables them, critical elements still display.
\n\n\nAnd be extra careful with parallax: my recommendation is to, at the very least, try limiting its use to the header (“hero”) only, and be mindful of getting a smooth, realistic parallax experience. My vertigo self would have said, “just don’t freaking use parallax. Never. EVER.” But I guess that might be a hard idea to sell to stakeholders and designers.
\n\n\nAlso consider learning how to use the prefers-reduced-motion feature query. This is a newer addition to the specs (it’s part of the Media Queries Level 5 module , which is at an early Editor’s Draft stage) that allows authors to apply selective styling depending on whether the user has requested the system to minimize the use of animations. OS and browser support for it is still quite limited, but the day will come when we will set any moving thing inside a query for when the user has no-preference, blocking animations from those who choose reduce.
\n\n\nAfter about a week of wrestling websites to provide a static experience, I remembered something that would prove to be my biggest ally while the vertigo lasted:
\n\n\nReader mode
\n\n\nSome browsers include a “reader mode” that strips the content from any styling choices, isolates it from any distraction, and provides a perfect WCAG compliant layout for the text to maximize readability.
\n\n\nIt is extremely helpful to provide a clear and consistent reading experience throughout multiple websites, especially for users with any kind of reading impairment.
\n\n\nI have to confess: before experiencing my vestibular disorder, I had never used Reader Mode (the formal name varies in browsers) or even checked if my projects were compatible with it. I didn’t even think it was such a useful feature, as a quick search for “reader mode” actually returned quite a few threads by users asking how to disable it or how to take the button for it out of Firefox’s address bar. (It seems some people are unwittingly activating it…perhaps the icon is not clear enough.)
\n\n\nDisplaying the button to access Reader Mode is toggled by browser heuristics, which are based on the use (or not) of semantic tags in a page’s HTML. Unfortunately this meant not all websites provided such a “luxury.”
\n\n\nI really wish I wouldn’t have to say this in 2019…but please, please use semantic tags. Correct conversational semantics allow your website to be displayed in Reader Mode, and provide a better experience for users of screen readers. Again, accessibility is intersectional.
\n\n\nReader Mode proved to be extremely useful while my vertigo lasted. But there was something even better:
\n\n\nDark color schemes
\n\n\nBy the fourth week, I started feeling mostly fine. I opened Visual Studio Code to try to get back to work. In doing so, it served me well to find one more revelation: a light-text-on-dark-background scheme was SO much easier for me to read. (Though I still was not able to return to work at this time.)
\n\n\nI was quite surprised, as I had always preferred light mode with dark-text-on-light-background for reading, and dark mode, with light-text-on-dark for coding. I didn’t know at the time that I was suffering from photophobia (which is a sensitivity to light), which was one of the reasons I found it hard to read on my desktop and to use my mobile device at all.
\n\n\nAs far as I know, photophobia is not a common symptom of vestibular disorders, but there are many conditions that will trigger it, so it’s worth looking into for our projects’ accessibility.
\n\n\nCSS is also planning a media query to switch color schemes. Known as prefers-color-scheme, it allows applying styles based on the user’s stated preference for dark or light theming. It’s also part of the Media Queries Level 5 spec, and at the time of writing this article it’s only available in Safari Technology Preview, with Mozilla planning to ship it in the upcoming Firefox 67. Luckily there’s a PostCSS plugin that allows us to use it in most modern browsers by turning prefers-color-schemequeries into color-index queries, which have much better support.
\n\n\nIf PostCSS is not your cup of tea, or for whatever reason you cannot use that approach to automate switching color schemes to a user’s preference, try at least to provide a theming option in your app’s configuration. Theming has become extremely simple since the release of CSS Custom Properties, so implementing this sort of switch is relatively easy and will greatly benefit anyone experiencing photophobia.
\n\n\nMoving on
\n\n\nAfter a month and some days, the vertigo disappeared completely, and I was able to return to work without needing any meds or further treatment. It should stay that way, as for most people it’s a once-in-a-lifetime occurrence.
\n\n\nI went back to my abled life, but the experience changed my mindset for good.
\n\n\nAs I said before, I always cared for making my projects compatible for people using keyboard navigation and screen readers. But I learned the hard way that there are plenty of “invisible conditions” that are just as important to take into consideration: vestibular disorders, cognitive differences, dyslexia, and color blindness, just to name a few. I was totally neglecting those most of the time, barely addressing the issues in order to pass automated tests, which means I was unintentionally annoying some users by making websites inaccessible to them.
\n\n\nAfter my experience with vertigo, I’ve turned to an accessibility-first approach to design and development. Now I ask myself, “am I leaving anyone behind with this decision?,” before dropping a single line of code. Accessibility should never be an afterthought.
\n\n\nMaking sure my projects work from the start for those with difficulties also improves the experience for everyone else. Think about how improving text styles for users with dyslexia, vertigo, or visual problems improves readability for all users, or how being able to control animations or choose a color scheme can be critical for users with attention disorders and photophobia, respectively, while also a nice feature for everybody.
\n\n\nIt also turned my workflow into a much smoother development experience, as addressing accessibility issues from the beginning can mean a slower start, but it’s also much easier and faster than trying to fix broken accessibility afterwards.
\n\n\nI hope that by sharing my personal experience with vertigo, I’ve illustrated how we can all design and develop a better web for everybody. Remember, we’re all just temporarily abled.
\n\n\n
\n
","direction":"ltr"},"alternate":[{"href":"http://feedproxy.google.com/~r/alistapart/main/~3/P6NK__5U2eQ/","type":"text/html"}],"canonical":[{"href":"https://alistapart.com/article/accessibility-for-vestibular/","type":"text/html"}],"crawled":1569828797486,"published":1569828797486,"origin":{"streamId":"feed/http://feeds.feedburner.com/alistapart/main","title":"A List Apart: The Full Feed","htmlUrl":"https://alistapart.com"},"visual":{"url":"http://www.blogcdn.com/www.engadget.com/media/2013/10/nvidia-shield-console-mode.jpg","width":620,"height":340,"contentType":"image/jpg"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/885f2e01-d314-4e63-abac-17dcb063f5b5","label":"Programming"}]},{"id":"Yn8gu9QqU/Nu8FbPliaB+X/354R2g5xNVMORFC3HITI=_16d8114a76e:1c44:90d684ff","keywords":["Application Development, JavaScript"],"originId":"https://alistapart.com/article/responsible-javascript-part-1/","fingerprint":"6e3f673d","title":"Responsible JavaScript: Part I","author":"by Jeremy Wagner","summary":{"content":"\nBy the numbers, JavaScript is a performance liability. If the trend persists, the median page will be shipping at least 400 KB of it before too long, and that’s merely what’s transferred. Like other text-based resources, JavaScript is almost always served compressed—but that might be the only thing we’re getting consistently right in its delivery.
\n\n\nUnfortunately, while reducing resource transfer time is a big part of that whole performance thing, compression has no effect on how long browsers take to process a script once it arrives in its entirety. If a server sends 400 KB of compressed JavaScript, the actual amount browsers have to process after decompression is north of a megabyte. How well devices cope with these heavy workloads depends, well, on the device. Much has been written about how adept various devices are at processing lots of JavaScript, but the truth is, the amount of time it takes to process even a trivial amount of it varies greatly between devices.
\n\n\nTake, for example, this throwaway project of mine, which serves around 23 KB of uncompressed JavaScript. On a mid-2017 MacBook Pro, Chrome chews through this comparably tiny payload in about 25 ms. On a Nokia 2 Android phone, however, that figure balloons to around 190 ms. That’s not an insignificant amount of time, but in either case, the page gets interactive reasonably fast.
\n\n\nNow for the big question: how do you think that little Nokia 2 does on an average page? It chokes. Even on a fast connection, browsing the web on it is an exercise in patience as JavaScript-laden web pages brick it for considerable stretches of time.
\n\n\n
Figure 1. A performance timeline overview of a Nokia 2 Android phone browsing on a page where excessive JavaScript monopolizes the main thread.\n\n\nWhile devices and the networks they navigate the web on are largely improving, we’re eating those gains as trends suggest. We need to use JavaScript responsibly. That begins with understanding what we’re building as well as how we’re building it.
\n\n\nThe mindset of “sites” versus “apps”
\n\n\nNomenclature can be strange in that we sometimes loosely identify things with terms that are inaccurate, yet their meanings are implicitly understood by everyone. Sometimes we overload the term “bee” to also mean “wasp”, even though the differences between bees and wasps are substantial. Those differences can motivate you to deal with each one differently. For instance, we’ll want to destroy a wasp nest, but because bees are highly beneficial and vulnerable insects, we may opt to relocate them.
\n\n\nWe can be just as fast and loose in interchanging the terms “website” and “web app”. The differences between them are less clear than those between yellowjackets and honeybees, but conflating them can bring about painful outcomes. The pain comes in the affordances we allow ourselves when something is merely a “website” versus a fully-featured “web app.” If you’re making an informational website for a business, you’re less likely to lean on a powerful framework to manage changes in the DOM or implement client-side routing—at least, I hope. Using tools so ill-suited for the task would not only be a detriment to the people who use that site but arguably less productive.
\n\n\nWhen we build a web app, though, look out. We’re installing packages which usher in hundreds—if not thousands—of dependencies, some of which we’re not sure are even safe. We’re also writing complicated configurations for module bundlers. In this frenzied, yet ubiquitous, sort of dev environment, it takes knowledge and vigilance to ensure what gets built is fast and accessible. If you doubt this, run npm ls --prod in your project’s root directory and see if you recognize everything in that list. Even if you do, that doesn’t account for third party scripts—of which I’m sure your site has at least a few.
\n\n\nWhat we tend to forget is that the environment websites and web apps occupy is one and the same. Both are subject to the same environmental pressures that the large gradient of networks and devices impose. Those constraints don’t suddenly vanish when we decide to call what we build “apps”, nor do our users’ phones gain magical new powers when we do so.
\n\n\nIt’s our responsibility to evaluate who uses what we make, and accept that the conditions under which they access the internet can be different than what we’ve assumed. We need to know the purpose we’re trying to serve, and only then can we build something that admirably serves that purpose—even if it isn’t exciting to build.
\n\n\nThat means reassessing our reliance on JavaScript and how the use of it—particularly to the exclusion of HTML and CSS—can tempt us to adopt unsustainable patterns which harm performance and accessibility.
\n\n\nDon’t let frameworks force you into unsustainable patterns
\n\n\nI’ve been witness to some strange discoveries in codebases when working with teams that depend on frameworks to help them be highly productive. One characteristic common among many of them is that poor accessibility and performance patterns often result. Take the React component below, for example:
\n\n\nimport React, { Component } from "react";\nimport { validateEmail } from "helpers/validation";\n\nclass SignupForm extends Component {\n constructor (props) {\n super(props);\n\n this.handleSubmit = this.handleSubmit.bind(this);\n this.updateEmail = this.updateEmail.bind(this);\n this.state.email = "";\n }\n\n updateEmail (event) {\n this.setState({\n email: event.target.value\n });\n }\n\n handleSubmit () {\n // If the email checks out, submit\n if (validateEmail(this.state.email)) {\n // ...\n }\n }\n\n render () {\n return (\n <div>\n <span class="email-label">Enter your email:</span>\n <input type="text" id="email" onChange={this.updateEmail} />\n <button onClick={this.handleSubmit}>Sign Up</button>\n </div>\n );\n }\n}
\n\n\nThere are some notable accessibility issues here:
\n\n\n- A form that doesn’t use a
<form> element is not a form. Indeed, you could paper over this by specifying role="form" in the parent <div>, but if you’re building a form—and this sure looks like one—use a <form> element with the proper action and method attributes. The action attribute is crucial, as it ensures the form will still do something in the absence of JavaScript—provided the component is server-rendered, of course. - A
<span> is not a substitute for a <label> element, which provides accessibility benefits <span>s don’t. - If we intend to do something on the client side prior to submitting a form, then we should move the action bound to the
<button> element's onClick handler to the <form> element’s onSubmit handler. - Incidentally, why use JavaScript to validate an email address when HTML5 offers form validation controls in almost every browser back to IE 10? There’s an opportunity here to rely on the browser and use an appropriate input type, as well as the
required attribute—but be aware that getting this to work right with screen readers takes a little know-how. - While not an accessibility issue, this component doesn't rely on any state or lifecycle methods, which means it can be refactored into a stateless functional component, which uses considerably less JavaScript than a full-fledged React component.
\n\n\nKnowing these things, we can refactor this component:
\n\n\nimport React from "react";\n\nconst SignupForm = props => {\n const handleSubmit = event => {\n // Needed in case we're sending data to the server XHR-style\n // (but will still work if server-rendered with JS disabled).\n event.preventDefault();\n\n // Carry on...\n };\n \n return (\n <form method="POST" action="/signup" onSubmit={handleSubmit}>\n <label for="email" class="email-label">Enter your email:</label>\n <input type="email" id="email" required />\n <button>Sign Up</button>\n </form>\n );\n};
\n\n\nNot only is this component now more accessible, but it also uses less JavaScript. In a world that’s drowning in JavaScript, deleting lines of it should feel downright therapeutic. The browser gives us so much for free, and we should try to take advantage of that as often as possible.
\n\n\nThis is not to say that inaccessible patterns occur only when frameworks are used, but rather that a sole preference for JavaScript will eventually surface gaps in our understanding of HTML and CSS. These knowledge gaps will often result in mistakes we may not even be aware of. Frameworks can be useful tools that increase our productivity, but continuing education in core web technologies is essential to creating usable experiences, no matter what tools we choose to use.
\n\n\nRely on the web platform and you’ll go far, fast
\n\n\nWhile we’re on the subject of frameworks, it must be said that the web platform is a formidable framework of its own. As the previous section showed, we’re better off when we can rely on established markup patterns and browser features. The alternative is to reinvent them, and invite all the pain such endeavors all but guarantee us, or worse: merely assume that the author of every JavaScript package we install has solved the problem comprehensively and thoughtfully.
\n\n\nSINGLE PAGE APPLICATIONS
\n\n\nOne of the tradeoffs developers are quick to make is to adopt the single page application (SPA) model, even if it’s not a fit for the project. Yes, you do gain better perceived performance with the client-side routing of an SPA, but what do you lose? The browser’s own navigation functionality—albeit synchronous—provides a slew of benefits. For one, history is managed according to a complex specification. Users without JavaScript—be it by their own choice or not—won’t lose access altogether. For SPAs to remain available when JavaScript is not, server-side rendering suddenly becomes a thing you have to consider.
\n\n\n
Figure 2. A comparison of an example app loading on a slow connection. The app on the left depends entirely upon JavaScript to render a page. The app on the right renders a response on the server, but then uses client-side hydration to attach components to the existing server-rendered markup.\n\n\nAccessibility is also harmed if a client-side router fails to let people know what content on the page has changed. This can leave those reliant on assistive technology to suss out what changes have occurred on the page, which can be an arduous task.
\n\n\nThen there’s our old nemesis: overhead. Some client-side routers are very small, but when you start with React, a compatible router, and possibly even a state management library, you’re accepting that there’s a certain amount of code you can never optimize away—approximately 135 KB in this case. Carefully consider what you’re building and whether a client side router is worth the tradeoffs you’ll inevitably make. Typically, you’re better off without one.
\n\n\nIf you’re concerned about the perceived navigation performance, you could lean on rel=prefetch to speculatively fetch documents on the same origin. This has a dramatic effect on improving perceived loading performance of pages, as the document is immediately available in the cache. Because prefetches are done at a low priority, they’re also less likely to contend with critical resources for bandwidth.
\n\n\n
Figure 3. The HTML for the writing/ URL is prefetched on the initial page. When the writing/ URL is requested by the user, the HTML for it is loaded instantaneously from the browser cache.\n\n\nThe primary drawback with link prefetching is that you need to be aware that it can be potentially wasteful. Quicklink, a tiny link prefetching script from Google, mitigates this somewhat by checking if the current client is on a slow connection—or has data saver mode enabled—and avoids prefetching links on cross-origins by default.
\n\n\nService workers are also hugely beneficial to perceived performance for returning users, whether we use client side routing or not—provided you know the ropes. When we precache routes with a service worker, we get many of the same benefits as link prefetching, but with a much greater degree of control over requests and responses. Whether you think of your site as an “app” or not, adding a service worker to it is perhaps one of the most responsible uses of JavaScript that exists today.
\n\n\nJAVASCRIPT ISN’T THE SOLUTION TO YOUR LAYOUT WOES
\n\n\nIf we’re installing a package to solve a layout problem, proceed with caution and ask “what am I trying to accomplish?” CSS is designed to do this job, and requires no abstractions to use effectively. Most layout issues JavaScript packages attempt to solve, like box placement, alignment, and sizing, managing text overflow, and even entire layout systems, are solvable with CSS today. Modern layout engines like Flexbox and Grid are supported well enough that we shouldn’t need to start a project with any layout framework. CSS is the framework. When we have feature queries, progressively enhancing layouts to adopt new layout engines is suddenly not so hard.
\n\n\n/* Your mobile-first, non-CSS grid styles goes here */\n\n/* The @supports rule below is ignored by browsers that don't\n support CSS grid, _or_ don't support @supports. */\n@supports (display: grid) {\n /* Larger screen layout */\n @media (min-width: 40em) {\n /* Your progressively enhanced grid layout styles go here */\n }\n}
\n\n\nUsing JavaScript solutions for layout and presentations problems is not new. It was something we did when we lied to ourselves in 2009 that every website had to look in IE6 exactly as it did in the more capable browsers of that time. If we’re still developing websites to look the same in every browser in 2019, we should reassess our development goals. There will always be some browser we’ll have to support that can’t do everything those modern, evergreen browsers can. Total visual parity on all platforms is not only a pursuit made in vain, it’s the principal foe of progressive enhancement.
\n\n\nI’m not here to kill JavaScript
\n\n\nMake no mistake, I have no ill will toward JavaScript. It’s given me a career and—if I’m being honest with myself—a source of enjoyment for over a decade. Like any long-term relationship, I learn more about it the more time I spend with it. It’s a mature, feature-rich language that only gets more capable and elegant with every passing year.
\n\n\nYet, there are times when I feel like JavaScript and I are at odds. I am critical of JavaScript. Or maybe more accurately, I’m critical of how we’ve developed a tendency to view it as a first resort to building for the web. As I pick apart yet another bundle not unlike a tangled ball of Christmas tree lights, it’s become clear that the web is drunk on JavaScript. We reach for it for almost everything, even when the occasion doesn’t call for it. Sometimes I wonder how vicious the hangover will be.
\n\n\nIn a series of articles to follow, I’ll be giving more practical advice to follow to stem the encroaching tide of excessive JavaScript and how we can wrangle it so that what we build for the web is usable—or at least more so—for everyone everywhere. Some of the advice will be preventative. Some will be mitigating “hair of the dog” measures. In either case, the outcomes will hopefully be the same. I believe that we all love the web and want to do right by it, but I want us to think about how to make it more resilient and inclusive for all.
\n\n\n
\n
","direction":"ltr"},"alternate":[{"href":"http://feedproxy.google.com/~r/alistapart/main/~3/QhDNVVA5eoQ/","type":"text/html"}],"canonical":[{"href":"https://alistapart.com/article/responsible-javascript-part-1/","type":"text/html"}],"crawled":1569828677486,"published":1569828677486,"origin":{"streamId":"feed/http://feeds.feedburner.com/alistapart/main","title":"A List Apart: The Full Feed","htmlUrl":"https://alistapart.com"},"visual":{"url":"http://www.blogcdn.com/www.engadget.com/media/2013/10/nvidia-shield-console-mode.jpg","width":620,"height":340,"contentType":"image/jpg"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/885f2e01-d314-4e63-abac-17dcb063f5b5","label":"Programming"}]},{"id":"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d72eabb28:1d8d:90d684ff","originId":"https://www.raywenderlich.com/5685025-android-fall-sale-giveaway-winners-and-last-day-for-discount","fingerprint":"7e246132","title":"Android Fall Sale Giveaway Winners – and Last Day for Discount! [FREE]","updated":1569591049000,"summary":{"content":"Check out the winners of our Android Fall Sale giveaway — and don’t forget today is the last day to grab the discount on our any of our Android books and limited-time bundles!","direction":"ltr"},"alternate":[{"href":"https://www.raywenderlich.com/5685025-android-fall-sale-giveaway-winners-and-last-day-for-discount","type":"text/html"}],"crawled":1569591049000,"published":1569591049000,"origin":{"streamId":"feed/http://www.raywenderlich.com/feed","title":"Ray Wenderlich | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more","htmlUrl":"http://www.raywenderlich.com/feed"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/885f2e01-d314-4e63-abac-17dcb063f5b5","label":"Programming"}]},{"keywords":["Miscellaneous"],"originId":"https://nshipster.com/ios-13","recrawled":1569511972636,"updateCount":1,"fingerprint":"a9d09fb8","id":"08l+9ftpGejQ9f/2DZ6dom5rSnNJJO9OCox6I3nUnWg=_16d689af5ed:46322:18991ffa","updated":1569222000000,"author":"Mattt","summary":{"direction":"ltr","content":"To mark last week’s release of iOS 13, we’re taking a look at some obscure (largely undocumented) APIs that you can now use in your apps.
"},"alternate":[{"href":"https://nshipster.com/ios-13/","type":"text/html"}],"crawled":1569418049005,"title":"iOS 13","published":1569222000000,"origin":{"streamId":"feed/http://nshipster.com/feed.xml","htmlUrl":"https://nshipster.com/","title":"NSHipster"},"content":{"direction":"ltr","content":"Apple announced a lot at WWDC this year.\nDuring the conference and the days that followed,\na lot of us walked around in a daze,\nattempting to recover from have our minds “hashtag-mindblown’d” (#\uD83E\uDD2F).\nBut now a few months later,\nafter everything announced has launched\n(well, almost everything)\nthose very same keynote announcements now elicit far different reactions:
\n\nAlthough the lion’s share of attention\nhas been showered on the aforementioned features,\nnot nearly enough coverage has been given to the rest of iOS 13 —\nand that’s a shame,\nbecause this release is among the most exciting in terms of new functionality\nand satisfying in terms of improving existing functionality.
\nSo to mark last week’s release of iOS 13,\nwe’re taking a look at some obscure (largely undocumented) APIs\nthat you can now use in your apps.\nWe’ve scavenged the best bits out of the\niOS 13 Release Notes\nAPI diffs,\nand now present them to you.
\nHere are some of our favorite things you can do starting in iOS 13:
\n
\n\nGenerate Rich Representations of URLs
\nNew in iOS 13,\nthe\nLinkPresentation framework\nprovides a convenient, built-in way to replicate the\nrich previews of URLs you see in Messages.\nIf your app has any kind of chat or messaging functionality,\nyou’ll definitely want to check this out.
\nRich previews of URLs have a rich history\ngoing at least as far back as the early ’00s,\nwith the spread of\nMicroformats\nby semantic web pioneers,\nand early precursors to Digg and Reddit\nusing khtml2png\nto generate thumbnail images of web pages.\nFast forward to 2010,\nwith the rise of social media and user-generated content,\nwhen Facebook created the OpenGraph protocol\nto allow web publishers to customize how their pages looked\nwhen posted on the Newsfeed.
\nThese days,\nmost websites reliably have OpenGraph <meta> tags on their site\nthat provide a summary of their content for\nsocial networks, search engines, and anywhere else that links are trafficked.\nFor example,\nhere’s what you would see if you did “View Source” for this very webpage:
\n<meta property="og:site_name" content="NSHipster" />\n <meta property="og:image" content="https://nshipster.com/logo.png" />\n <meta property="og:type" content="article" />\n <meta property="og:title" content="iOS 13" />\n <meta property="og:url" content="https://nshipster.com/ios-13/" />\n <meta property="og:description" content="To mark last week's release of iOS 13, we're taking a look at some obscure (largely undocumented) APIs that you can now use in your apps." />\n
\nIf you wanted to consume this information in your app,\nyou can now use the LinkPresentation framework’s LPMetadataProvider class\nto fetch the metadata and optionally construct a representation:
\nimport LinkPresentation\n let metadataProvider = LPMetadataProvider()\n let url = URL(string: "https://nshipster.com/ios-13/")!\n let metadataProvider = LPMetadataProvider()\n metadataProvider.startFetchingMetadata(for: url) { [weak self] metadata, error in\n guard let metadata = metadata else { return }\n let linkView = LPLinkView(metadata: metadata)\n self?.view.addSubview(linkView)\n }\n
\nAfter setting appropriate constraints\n(and perhaps a call to sizeToFit()),\nyou’ll get the following,\nwhich the user can tap to preview the linked webpage:
\n\n\n
\n\n\nAlternatively,\nif you already have the metadata in-app,\nor otherwise can’t or don’t want to fetch remotely,\nyou can construct an LPLinkMetadata directly:
\nlet metadata = LPLinkMetadata()\n metadata.url = url\n metadata.title = "iOS 13"\n metadata.iconProvider = ...\n let linkView = LPLinkView(metadata: metadata)\n
\n\n\nPerform On-Device Speech Recognition
\nSFSpeechRecognizer\ngets a major upgrade in iOS 13 —\nmost notably for its added support for on-device speech recognition.
\nPreviously,\ntranscription required an internet connection\nand was restricted to a maximum of 1-minute duration\nwith daily limits for requests.\nBut now,\nyou can do speech recognition completely on-device and offline,\nwith no limitations.\nThe only caveats are that\noffline transcription isn’t as good as what you’d get with a server connection,\nand is only available for certain languages.
\nTo determine whether offline transcription is available for the user’s locale,\ncheck the SFSpeechRecognizer property\nsupportsOnDeviceRecognition.\nAt the time of publication, the list of supported languages are as follows:
\n\n\n- English
\n- United States (
en-US) \n- Canada (
en-CA) \n- Great Britain (
en-GB) \n- India (
en-IN) \n- Spanish
\n- United States (
es-US) \n- Mexico (
es-MX) \n- Spain (
es-ES) \n- Italian
\n- Italy (
it-IT) \n- Portuguese
\n- Brazil (
pt-BR) \n- Russian
\n- Russia (
ru-RU) \n- Turkish
\n- Turkey (
tr-TR) \n- Chinese
\n- Mandarin (
zh-cmn) \n- Cantonese (
zh-yue) \n
\n\n\nBut that’s not all for speech recognition in iOS 13!\nSFSpeechRecognizer now provides information including\nspeaking rate and average pause duration,\nas well as voice analytics features like\njitter (variations in pitch) and\nshimmer (variations in amplitude).
\nimport Speech\n guard SFSpeechRecognizer.authorizationStatus() == .authorized\n let recognizer = SFSpeechRecognizer()\n else {\n fatalError()\n }\n let url: URL = ...\n let request = SFSpeechURLRecognitionRequest(url: url)\n recognizer.recognitionTask(with: request) { (result, error) in\n guard let result = result else { return }\n for segment in result.bestTranscription.segments {\n guard let voiceAnalytics = segment.voiceAnalytics else { continue }\n let pitch = voiceAnalytics.pitch.acousticFeatureValuePerFrame\n let voicing = voiceAnalytics.voicing.acousticFeatureValuePerFrame\n let jitter = voiceAnalytics.jitter.acousticFeatureValuePerFrame\n let shimmer = voiceAnalytics.shimmer.acousticFeatureValuePerFrame\n }\n }\n
\nInformation about pitch and voicing and other features\ncould be used by your app\n(perhaps in coordination with CoreML)\nto differentiate between speakers\nor determine subtext from a speaker’s inflection.
\n\nSend and Receive Web Socket Messages
\nSpeaking of the Foundation URL Loading System,\nwe now have native support for\nsomething that’s been at the top of our wish list for many years:\nweb sockets.
\nThanks to the new\nURLSessionWebSocketTask class\nin iOS 13,\nyou can now incorporate real-time communications in your app\nas easily and reliably as sending HTTP requests —\nall without any third-party library or framework:
\nlet url = URL(string: "wss://...")!\n let webSocketTask = URLSession.shared.webSocketTask(with: url)\n webSocketTask.resume()\n // Send one message\n let message: URLSessionWebSocketTask.Message = .string("Hello, world!")\n webSocketTask.send(message) { error in\n ...\n }\n // Receive one message\n webSocketTask.receive { result in\n guard case let .success(message) = result else { return }\n ...\n }\n // Eventually...\n webSocketTask.cancel(with: .goingAway, reason: nil)\n
\n\nFor years now,\nnetworking has been probably the fastest-moving part\nof the whole Apple technology stack.\nEach WWDC,\nthere’s so much to talk about that\nthat they routinely have to break their content across two separate sessions.\n2019 was no exception,\nand we highly recommend that you take some time to check out this year’s\n“Advances in Networking” sessions\n(Part 1,\nPart 2).
\n\nDo More With Maps
\nMapKit is another part of Apple SDKs\nthat’s consistently had a strong showing at WWDC year after year.\nAnd it’s often the small touches that make the most impact in our day-to-day.
\nFor instance,\nthe new\nMKMapView.CameraBoundary API\nin iOS 13\nmakes it much easier to constrain a map’s viewport to a particular region\nwithout locking it down completely.
\nlet region = MKCoordinateRegion(center: mapView.center,\n latitudinalMeters: 1000,\n longitudinalMeters: 1000)\n mapView.cameraBoundary = MKMapView.CameraBoundary(coordinateRegion: region)\n
\nAnd with the new\nMKPointOfInterestFilter API,\nyou can now customize the appearance of map views\nto show only certain kinds of points of interest\n(whereas previously it was an all-or-nothing proposition).
\nlet filter = MKPointOfInterestFilter(including: [.cafe])\n mapView.pointOfInterestFilter = filter // only show cafés\n
\nFinally,\nwith MKGeoJSONDecoder,\nwe now have a built-in way to pull in GeoJSON shapes\nfrom web services and other data sources.
\nlet decoder = MKGeoJSONDecoder()\n if let url = URL(string: "..."),\n let data = try? Data(contentsOfURL: url),\n let geoJSONObjects = try? decoder.decode(data) {\n for case let overlay as MKOverlay in geoJSONObjects {\n mapView.addOverlay(overlay)\n }\n }\n
\n\nKeep Promises in JavaScript
\nIf you enjoyed our article about JavaScriptCore,\nyou’d be thrilled to know that JSValue objects\nnow natively support promises.
\nFor the uninitiated:\nin JavaScript, a Promise\nis an object that represents the eventual completion (or rejection)\nof an asynchronous operation\nand its resulting value.\nPromises are a mainstay of modern JS development —\nperhaps most notably within the fetch API.
\nAnother addition to JavaScriptCore in iOS 13\nis support for symbols\n(no, not those symbols).\nFor more information about\ninit(newSymbolFromDescription:in:),\nrefer to the docs\njust guess how to use it.
\n\nRespond to Objective-C Associated Objects (?)
\nOn a lark,\nwe decided to see if there was anything new in Objective-C this year\nand were surprised to find out about\nobjc_setHook_setAssociatedObject.\nAgain, we don’t have much to go on except the declaration,\nbut it looks like you can now configure a block to execute when\nan associated object is set.\nFor anyone still deep in the guts of the Objective-C runtime,\nthis sounds like it could be handy.
\n\nTame Activity Items (?)
\nOn the subject of missing docs:\nUIActivityItemsConfiguration\nseems like a compelling option for managing\nactions in the new iOS 13 share sheets,\nbut we don’t really know where to start…
\n
\nShame that we don’t have the information we need to take advantage of this yet.
\n\nFormat Lists and Relative Times
\nAs discussed in a previous article,\niOS 13 brings two new formatters to Foundation:\nListFormatter\nand\nRelativeDateTimeFormatter.
\nNot to harp on about this,\nbut both of them are still undocumented,\nso if you want to learn more,\nwe’d recommend checking out that article from July.\nOr, if you’re in a rush\nhere’s a quick example demonstrating how to use both of them together:
\nimport Foundation\n let relativeDateTimeFormatter = RelativeDateTimeFormatter()\n relativeDateTimeFormatter.dateTimeStyle = .named\n let listFormatter = ListFormatter()\n listFormatter.string(from: [\n relativeDateTimeFormatter.localizedString(from: DateComponents(day: -1)),\n relativeDateTimeFormatter.localizedString(from: DateComponents(day: 0)),\n relativeDateTimeFormatter.localizedString(from: DateComponents(day: 1))\n ]) // "yesterday, today, and tomorrow"\n
\n\nTrack the Progress of Enqueued Operations
\nStarting in iOS 13,\nOperationQueue now has a\nprogress property.
\nGranted,\n(NS)Progress\nobjects aren’t the most straightforward or convenient things to work with\n(we’ve been meaning to write an article about them at some point),\nbut they have a complete and well-considered API,\nand even have some convenient slots in app frameworks.
\nFor example,\ncheck out how easy it is to wire up a UIProgressView\nto display the live-updating progress of an operation queue\nby way of its observedProgress property:
\nimport UIKit\n fileprivate class DownloadOperation: Operation { ... }\n class ViewController: UIViewController {\n private let operationQueue = {\n let queue = OperationQueue()\n queue.maxConcurrentOperationCount = 1\n }()\n @IBOutlet private var progressView: UIProgressView!\n @IBAction private func startDownloading(_ sender: Any) {\n operationQueue.cancelAllOperations()\n progressView.observedProgress = operationQueue.progress\n for url in [...] {\n let operation = DownloadOperation(url: url)\n operationQueue.addOperation(operation)\n }\n }\n }\n
\nIt’s also worth mentioning a few other APIs coming to in 13,\nlike\nschedule(after:interval:tolerance:options:_:),\nwhich clues OperationQueue into the new\nCombine framework\nin a nice way,\nand addBarrierBlock(_:),\nwhich presumably works like\nDispatch barrier blocks\n(though without documentation, it’s anyone’s guess).
\n\nManage Background Tasks with Ease
\nOne of the things that often differentiates category-defining apps from their competitors\nis their use of background tasks\nto make sure the app is fully synced and updated for the next time\nit enters the foreground.
\niOS 7 was the first release to provide\nan official API for scheduling background tasks\n(though developers had employed various creative approaches prior to this).\nBut in the intervening years,\nmultiple factors —\nfrom an increase in iOS app capabilities and complexity\nto growing emphasis in performance, efficiency, and privacy for apps —\nhave created a need for a more comprehensive solution.
\nThat solution came to iOS 13 by way of the new\nBackgroundTasks framework.
\nAs described in this year’s WWDC session\n“Advances in App Background Execution”,\nthe framework distinguishes between two broad classes of background tasks:
\n\n- \napp refresh tasks:\nshort-lived tasks that keep an app up-to-date throughout the day
\n- \nbackground processing tasks:\nlong-lived tasks for performing deferrable maintenance tasks
\n
\nThe WWDC session and the accompanying sample code project\ndo a great job of explaining how to incorporate both of these\ninto your app.\nBut if you want the quick gist of it,\nhere’s a small example of an app that schedules periodic refreshes\nfrom a web server:
\nimport UIKit\n import BackgroundTasks\n fileprivate let backgroundTaskIdentifier = "com.nshipster.example.task.refresh"\n @UIApplicationMain\n class AppDelegate: UIResponder, UIApplicationDelegate {\n var window: UIWindow?\n lazy var backgroundURLSession = {\n let configuration = URLSessionConfiguration.background(withIdentifier: "com.nshipster.url-session.background")\n configuration.discretionary = true\n configuration.timeoutIntervalForRequest = 30\n return URLSession(configuration: configuration, delegate: ..., delegateQueue: ...)\n }\n func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {\n ...\n BGTaskScheduler.shared.register(forTaskWithIdentifier: backgroundTaskIdentifier, using: nil) { task in\n self.handleAppRefresh(task: task as! BGAppRefreshTask)\n }\n return true\n }\n func applicationDidEnterBackground(_ application: UIApplication) {\n scheduleAppRefresh()\n }\n func scheduleAppRefresh() {\n let request = BGAppRefreshTaskRequest(identifier: backgroundTaskIdentifier)\n request.earliestBeginDate = Date(timeIntervalSinceNow: 60 * 10)\n do {\n try BGTaskScheduler.shared.submit(request)\n } catch {\n print("Couldn't schedule app refresh: \\(error)")\n }\n }\n func handleAppRefresh(task: BGAppRefreshTask) {\n scheduleAppRefresh()\n let url: URL = ...\n var dataTask = backgroundURLSession.dataTask(with: url) { (data, response, error) in\n ...\n let success = (200..<300).contains(response?.statusCode)\n task.setTaskCompleted(success: success)\n }\n task.expirationHandler = {\n dataTask.cancel()\n }\n dataTask.resume()\n }\n ...\n }\n
\n\n\nAnnotate Text Content Types for Better Accessibility
\nYou know how frustrating it is to hear some people read out URLs?\n(“eɪʧ ti ti pi ˈkoʊlən slæʃ slæʃ ˈdʌbəlju ˈdʌbəlju ˈdʌbəlju dɑt”…)\nThat’s what it can be like when\nVoiceOver\ntries to read something without knowing more about what it’s reading.
\niOS 13 promises to improve the situation considerably\nwith the new accessibilityTextualContext property\nand UIAccessibilityTextAttributeContext NSAttributedString attribute key.\nWhenever possible,\nbe sure to annotate views and attributed strings with\nthe constant that best describes the kind of text being displayed:
\n\nUIAccessibilityTextualContextConsole \nUIAccessibilityTextualContextFileSystem \nUIAccessibilityTextualContextMessaging \nUIAccessibilityTextualContextNarrative \nUIAccessibilityTextualContextSourceCode \nUIAccessibilityTextualContextSpreadsheet \nUIAccessibilityTextualContextWordProcessing \n
\n\n\nRemove Implicitly Unwrapped Optionals from View Controllers Initialized from Storyboards
\nSwiftUI may have signaled the eventual end of Storyboards,\nbut that doesn’t mean that things aren’t and won’t continue to get better\nuntil if / when that day comes.
\nOne of the most irritating anti-patterns for Swift purists\nwhen working on iOS projects with Storyboards\nhas been view controller initialization.\nDue to an impedance mismatch between\nInterface Builder’s “prepare for segues” approach and\nSwift’s object initialization rules,\nwe frequently had to choose between\nmaking all of our properties non-private, variable, and (implicitly unwrapped) optionals,\nor foregoing Storyboards entirely.
\nXcode 11 and iOS 13 allow these paradigms to reconcile their differences\nby way of the new @IBSegueAction attribute\nand some new UIStoryboard class methods:
\nFirst,\nthe @IBSegueAction attribute\ncan be applied view controller method declarations\nto designate itself as the API responsible for\ncreating a segue’s destination view controller\n(i.e. the destinationViewController property of the segue parameter\nin the prepare(for:sender:) method).
\n@IBSegueAction\n func makeProfileViewController(coder: NSCoder, sender: Any?, segueIdentifier: String?) -> ProfileViewController? {\n ProfileViewController(\n coder: coder,\n name: self.selectedName,\n avatarImageURL: self.selectedAvatarImageURL\n )\n }\n
\nSecond,\nthe UIStoryboard class methods\ninstantiateInitialViewController(creator:)\nand instantiateViewController(identifier:creator:)\noffer a convenient block-based customization point for\ninstantiating a Storyboard’s view controllers.
\nimport UIKit\n struct Person { ... }\n class ProfileViewController: UIViewController {\n let name: String\n let avatarImageURL: URL?\n init?(coder: NSCoder, name: String, avatarImageURL: URL?) {\n self.name = name\n self.avatarImageURL = avatarImageURL\n super.init(coder: coder)\n }\n required init?(coder: NSCoder) {\n fatalError("init(coder:) has not been implemented")\n }\n }\n let storyboard = UIStoryboard(name: "ProfileViewController", bundle: nil)\n storyboard.instantiateInitialViewController(creator: { decoder in\n ProfileViewController(\n coder: decoder,\n name: "Johnny Appleseed",\n avatarImageURL: nil\n )\n })\n
\nTogether with the new UIKit Scene APIs,\niOS 13 gives us a lot to work with\nas we wait for SwiftUI to mature and stabilize.
\n\n
\nThat does it for our round-up of iOS 13 features\nthat you may have missed.\nBut rest assured —\nwe’re planning to cover many more new APIs in future NSHipster articles.
\nIf there’s anything we missed that you’d like for us to cover,\nplease @ us on Twitter!
"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/885f2e01-d314-4e63-abac-17dcb063f5b5","label":"Programming"}]},{"keywords":["Cocoa"],"originId":"https://nshipster.com/apns-device-tokens","fingerprint":"a5ee91d6","id":"08l+9ftpGejQ9f/2DZ6dom5rSnNJJO9OCox6I3nUnWg=_16d689af5ed:46321:18991ffa","updated":1568617200000,"author":"Mattt","summary":{"direction":"ltr","content":"Precedent plays an important role throughout software engineering. So what does a change in iOS 13 to push notification registration have to say about Apple’s API design decisions?
"},"alternate":[{"href":"https://nshipster.com/apns-device-tokens/","type":"text/html"}],"crawled":1569418049005,"title":"Apple Push Notification Device Tokens","published":1568617200000,"origin":{"streamId":"feed/http://nshipster.com/feed.xml","htmlUrl":"https://nshipster.com/","title":"NSHipster"},"content":{"direction":"ltr","content":"In law,\nthe latin phrase\nstare decisis (“to stand by things decided”)\nis often used to refer to the doctrine of precedent —\nthe idea that,\nwhen deciding a case,\na court should look to previous decisions made\nfor cases with similar facts and scenarios.\nThis principle serves as a foundation of the American legal system,\nand the English common law from which it derives.
\nFor example,\nconsider Apple v. Pepper,\nwhich was argued before the Supreme Court of the United States\nin its most recent session\nand sought to settle the following question:
\n\nIf Apple and its App Store constitute a monopoly,\ncan consumers sue Apple for offering apps at higher-than-competitive prices,\neven when the prices are set by third-party developers?
\n
\nIn its decision,\nthe Court relied on precedent set in 1977\nby a case known as Illinois Brick,\nwhich itself affirmed a ruling made a decade earlier\nin a case called Hanover Shoe.\nOn its face,\niPhones in 2010’s would seem to have little to do with bricks from the 1970’s\n(aside from the obvious connotation),\nbut within the context of\nUnited States antitrust law,\nthe connection between the two was inescapable.
\n\nAdherence to precedence confers inertia in the decision-making process.\nIt promotes stability throughout the legal system\nand the institutions that rely on a consistent application of laws.
\nHowever,\nlike inertia,\nprecedence can also be overcome with sufficiently compelling reasons;\nwe are bound by the past only insofar as to give it due consideration.
\n
\nBearing all of that in mind,\nlet’s smash cut\nto our subject for this week’s brief:\nApple Push Notification Device Tokens —\nand in particular,\na single change in iOS 13 that may incidentally break push notifications\nfor thousands of apps.
\n\nA Push Notifications Primer
\nPush notifications allow apps to communicate with users\nin response to remote events when they aren’t currently in use.
\nUnlike SMS or email,\nwhich allows a sender to communicate with a recipient directly\nusing a unique identifier (a phone number and email address, respectively),\ncommunication between the app’s remote server and the user’s local device\nare facilitated by the Apple Push Notification service\n(APNs).
\nHere’s how that works:
\n\nThe deviceToken parameter in the app delegate method\nis an opaque Data value —\nkind of like a really long unique phone number or email address —\nthat the app’s push notification provider uses\nto route notifications through APNs to reach\nthis particular installation of the app.
\nIn principle,\nrepresenting this parameter as a Data value makes a lot of sense —\nthe value itself is meaningless.\nHowever, in practice,\nthis API design decision has been the source of untold amounts of heartache.
\n\nThe Enduring Challenges of Sending Device Tokens Back to the Server
\nWhen the app delegate receives its deviceToken,\nthat’s not the end of the story.\nIn order for its to be used to send push notifications,\nit needs to be sent from the client to the server.
\nThe question is, “How”?
\nBefore you jump to a particular answer,\nconsider the historical context of iOS 3 (circa 2009),\nwhen push notifications were first introduced:
\n\n“Back in My Day…“\n
\nYou could create an NSURLRequest object,\nset its httpBody property to the deviceToken,\nand send it using NSURLConnection,\nbut you’d probably also want to include some additional information —\nlike a username or email address —\nto associate it with an account in the app.\nThat meant that the data you set as a request’s HTTP body\ncouldn’t just be the device token.
\nSending an HTTP POST body withapplication/x-www-form-urlencoded\n(e.g. username=jappleseed&deviceToken=____)\nis one possibility for encoding multiple fields into a single payload,\nbut then the question becomes,\n“How do you encode binary data into text?”
\nBase64\nis a great binary-to-text encoding strategy,\nbut NSData -base64EncodedStringWithOptions:\nwouldn’t be available until iOS 7,\nfour years after push notifications were first introduced in iOS 3.\nWithout CocoaPods or a strong open-source ecosystem\nto fill in the gaps,\nyou were left to follow\nblog posts\ndescribing how to roll your own implementation,\nhoping that things would work as advertised.
\n\nGiven the complexity in using Base64 encoding on iOS < 7,\nmost developers instead opted to take advantage of\nwhat they saw as an easier, built-in alternative:
\n\nNSData, in its Own Words
\nDevelopers,\nin an attempt to understand what exactly\nthis deviceToken parameter was,\nwould most likely have passed it into an NSLog statement:
\nNSLog(@"%@", deviceToken);\n // Prints "<965b251c 6cb1926d e3cb366f dfb16ddd e6b9086a 8a3cac9e 5f857679 376eab7C>"\n
\nUnfortunately,\nfor developers less versed in matters of data and encoding,\nthis output from NSLog may have led them astray:
\n“Oh wow, so deviceToken is actually a string!\n(I wonder why Apple was making this so difficult in the first place).\nBut no matter — I can take it from here.”
\n// ⚠️ Warning: Don't do this\n NSString *token = [[[[deviceToken description]\n stringByReplacingOccurrencesOfString:@" " withString:@""]\n stringByReplacingOccurrencesOfString:@"<" withString:@""]\n stringByReplacingOccurrencesOfString:@">" withString:@""];\n
\nIt’s unclear whether push notification service providers spurred this practice\nby requiring Base16 / hexadecimal representations from the beginning,\nor if they adopted it in response to how folks were\nalready accustomed to doing it,\nbut either way,\nthe practice stuck.\nAnd for nearly a decade,\nthis was how a significant percentage of apps were handling\npush notification device token registration.
\nThat was until Swift 3 and iOS 10.
\n\n\nRelitigating the Past with Swift 3
\nBy 2016,\nSwift had stabilized and matured to the point that\nmost if not many developers were choosing to write new apps in Swift,\nor at least write all new code in Swift for existing apps.
\nFor those who did,\nthe transition to Swift 3\nwas most memorable for its painful migration from Swift 2.\nAs part of “the grand API renaming”\ncommon Foundation types, including NSData,\ndropped their NS prefix in APIs,\nusing a bridged, Swift value type in its place.\nFor the most part,\nthings worked as expected.\nBut there were a few differences in behavior —\nlargely undocumented or incidental behavior\nthat caused a breaking change.\nFor example,\nconsider the following change in\napplication(_:didRegisterForRemoteNotificationsWithDeviceToken:):
\n// Swift 2: deviceToken is NSData\n deviceToken.description // "<965b251c 6cb1926d e3cb366f dfb16ddd e6b9086a 8a3cac9e 5f857679 376eab7C>"\n // Swift 3: deviceToken is Data\n deviceToken.description // "32 bytes"\n
\nHowever,\nmany developers remained undeterred by what was seen as a minor annoyance,\nand worked around the issue by recasting to NSData and its former behavior:
\n// ⚠️ Warning: Don't do this\n let tokenData = deviceToken as NSData\n let token = tokenData.description\n let token = "\\(deviceToken)".replacingOccurrences(of: " ", with: "")\n .replacingOccurrences(of: "<", with: "")\n .replacingOccurrences(of: ">", with: "")\n
\nOnce again,\ndoing things the wrong way\nmanaged to keep things working for another couple years.
\nBut that’s all coming to an end with iOS 13.
\n\n\nOverturned in iOS 13
\niOS 13 changes the format of descriptions\nfor Foundation objects,\nincluding NSData:
\n// iOS 12\n (deviceToken as NSData).description // "<965b251c 6cb1926d e3cb366f dfb16ddd e6b9086a 8a3cac9e 5f857679 376eab7C>"\n // iOS 13\n (deviceToken as NSData).description // "{length = 32, bytes = 0x965b251c 6cb1926d e3cb366f dfb16ddd ... 5f857679 376eab7c }"\n
\nWhereas previously,\nyou could coerce NSData to spill its entire contents\nby converting it into a String,\nit now reports its length and a truncated summary of its internal bytes.
\nSo from now on,\nif you need to convert your push notification registration deviceToken\ninto a Base16-encoded / hexadecimal string representation,\nyou should do the following:
\nlet deviceTokenString = deviceToken.map { String(format: "%02x", $0) }.joined()\n
\nFor clarity, let’s break this down and explain each part:
\n\n- The
map method operates on each element of a sequence.\nBecause Data is a sequence of bytes in Swift,\nthe passed closure is evaluated for each byte in deviceToken. \n- The
String(format:) initializer\nevaluates each byte in the data\n(represented by the anonymous parameter $0)\nusing the %02x format specifier,\nto produce a zero-padded, 2-digit hexadecimal representation of\nthe byte / 8-bit integer. \n- After collecting each byte representation created by the
map method,\njoined() concatenates each element into a single string. \n
\n\n
\nWas Apple irresponsible in making this particular change?
\nWe’d argue: No, not really.
\nDevelopers shouldn’t have relied on a specific format for\nan object’s description.
\nDumping an entire Data value’s contents becomes untenable at a certain point,\nand this change to a more succinct summary\nmakes debugging larger data blobs significantly easier.
\n
\nLike we said about laws at the start of this article,\nprecedence is a form of inertia,\nnot an immutable truth.
\nStare decisis plays an important role\nthroughout software engineering.\nExamples like the “Referer” [sic] header” —\neven the conventions we have about\nthe direction of electricity flow —\ndemonstrate the value of sticking to decisions,\nunless an opportunity arises to compel change.
"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/885f2e01-d314-4e63-abac-17dcb063f5b5","label":"Programming"}]},{"keywords":["Miscellaneous"],"originId":"https://nshipster.com/dark-mode","fingerprint":"49169576","id":"08l+9ftpGejQ9f/2DZ6dom5rSnNJJO9OCox6I3nUnWg=_16d689af5ed:46320:18991ffa","updated":1567407600000,"author":"Mattt","summary":{"direction":"ltr","content":"After waiting for what felt like an eternity (but was only, like, a year), Dark Mode is now finally coming to the iPhone and iPad iOS 13. But will your app be ready on launch day?
"},"alternate":[{"href":"https://nshipster.com/dark-mode/","type":"text/html"}],"crawled":1569418049005,"title":"Dark Mode on iOS 13","published":1567407600000,"origin":{"streamId":"feed/http://nshipster.com/feed.xml","htmlUrl":"https://nshipster.com/","title":"NSHipster"},"content":{"direction":"ltr","content":"Today is Labor Day in the United States\n(and Labour Day in Canada),\na day to celebrate the achievement of the workers\nwho organized to bring about fair and safe conditions for employees —\nprotections that serve as the foundation for the modern workplace.
\n\nLabor Day is also the unofficial end of summer;\nthe long weekend acting as a buffer between\nthe lemonades, sunburns, and trashy romance novels of June and\nthe pumpkin spice lattes, flu shots, and textbooks of September.
\nHowever,\nfor the stereotypical tech worker,\nwho likes the heat of summer about as much as\nthe concept of “work/life balance”,\nLabor Day frequently serves as something else entirely:\na wake-up call.\nThat,\nafter a lazy summer of ideation and experimentation,\nit’s once again time to launch new products and ship new features.\nAnd if you’re an app developer specifically,\nyou may know today as,\n“Oh-\uD83E\uDD2C-it’s-September-and-I-still-haven’t-implemented-Dark-Mode-in-my-app” day.
\nWe’re dedicating this week’s article to anyone out there\nwho’s celebrating this esteemed holiday,\nwhether contemporaneously or in the intervening days until iOS 13 goes GM.\nWe hope that a few quick tips can help shed light\nagainst the shade of your looming deadline.
\n
\n
\nDark Mode is an appearance preference\nthat tells the system and participating apps to adopt darker color palettes.\nWhereas an app may display dark text on a light background by default,\nit may instead show white text on a dark background.
\nLast year,\nDark Mode was the killer feature of macOS 10.14 Mojave,\nand its contemporaneous launch with Safari 12\nrippled throughout the World-Wide Web,\ngaining steady adoption among websites\n(like yours truly)\nand\nother browsers.
\nAfter waiting for what felt like an eternity\n(but was only, like, a year),\nDark Mode is now finally coming to the iPhone and iPad iOS 13.\nNo longer will we have to tinker with\nDisplay Accommodations\nto limit our light pollution when browsing Reddit late at night.
\n\nAdopting Dark Mode on iOS
\nApple’s done a great job designing a flexible, convenient API\nand providing excellent documentation to go with it.
\nOf course, the challenge with Apple technologies\nis that short of telling you that “you’re holding it wrong”,\nthey’ll rarely acknowledge the existence of prior art or alternatives,\nlet alone provide a transition document that in any way resembles\nhow everyone was doing things before we got an officially-sanctioned API.\n(Then again, can you really blame them?)
\nIf you were following 100% of Apple’s guidance to the letter,\nyou’d barely have to change a line or code\nto get your app ready for\nnext week’s special event.\nBut most apps are built on solutions we built for ourselves\nto bridge the gaps in the SDK,\nand it may not be clear how to get on the new\nhappy path\nfrom there.
\nApple’s guidance for adopting Dark Mode is fantastic for new projects\nbut doesn’t quite hit all of the points you should be aware of\nwhen preparing your existing app for iOS 13.\nSo without further ado,\nhere’s 6 action items for how to get your app ready for Dark Mode.
\n
\n\n#Cancel Color Literals
\nIn Xcode,\na color literal\nis code with the prefix #colorLiteral\nthat is rendered as a color swatch in the editor.\nFor example,\nthe code #colorLiteral(red: 1, green: 0.67, blue: 0, alpha: 1)\nis rendered in Xcode as .\nColor literals can be drag-and-dropped from\nthe Media Browser in Xcode 10,\nwhich has been consolidated with Snippets into the new Library panel in Xcode 11.
\nBoth color literals and their cousin, image literals,\nwere introduced in support of Xcode Playgrounds.\nBut don’t let their appearance fool you:\nneither have a place in your app’s codebase.
\n\nDark Mode or not,\nyou can replace all usage of color literals throughout your codebase\nfrom the command line:
\n$ find . -name '*.swift' \\\n -exec sed -i '' -E 's/#colorLiteral\\(red: (.*), green: (.*), blue: (.*), alpha: (.*)\\)/UIColor(red: \\1, green: \\2, blue: \\3, alpha: \\4)/ {} \\;\n
\nBut before you do,\nyou might as well do one better\nand replace it with something that will stand the test of time.
\n\nNix UIColor Hexadecimal Initializers
\nAmong the most common extensions you’ll find in a\nSwiss Army Knife-style CocoaPod\nis a category on UIColor that initializes from a hexadecimal string.\nSomething along the lines of this:
\nimport SwiftySwiftColorSwift\n let orange = UIColor(hex: "#FB8C00") // \uD83D\uDC4E\n
\nSetting aside any qualms about how they’re typically employed and implemented,\nyou’d be well-advised to have these go the way of color literals\nfor the same reasons we described in the previous section.
\nBut worry not!\nYou’ll still have a way to define colors\nusing those hex codes that your designer sent over,\nas we’ll see in our discussion of named colors.
\n\nFind & Replace Fixed Colors
\nUIColor defines several class properties\nthat return colors by their common name.\nThese properties are problematic in iOS 13,\nbecause they don’t automatically adjust for light or dark appearance.\nFor example,\nsetting a label’s color to .black looks fine\nagainst the default UITableViewCell background,\nbut it’s illegible when that background becomes black\nwhen Dark Mode is enabled.
\nTo make your app ready for Dark Mode on iOS 13,\nyou’ll most likely want to replace any instance of the following\nUIColor class properties:
\n\n
\nred \norange \nyellow \nbrown \ngreen \ncyan \nblue \npurple \nmagenta \n
\n
\n
\nwhite \nlightGray \ngray \ndarkGray \nblack \n
\n
\nHopefully you aren’t using the built-in\nROYGBIV\nUIColor constants for much other than occasional layout debugging,\nbut chances you’ll probably find a few instances of .black or .white\npeppered throughout your codebase somewhere.
\nIn any case,\nthe easiest change to support Dark Mode would be to\nreplace any of the aforementioned fixed color properties with\nthe corresponding system-prefixed adaptable color below:
\n\n\n\n\n| Name | \nAPI | \nLight | \nDark | \n
\n\n| Default | \nAccessible | \nDefault | \nAccessible | \n
\n\n\n\n| Red | \nsystemRed | \n | \n | \n | \n | \n
\n\n| Orange | \nsystemOrange | \n | \n | \n | \n | \n
\n\n| Yellow | \nsystemYellow | \n | \n | \n | \n | \n
\n\n| Green | \nsystemGreen | \n | \n | \n | \n | \n
\n\n| Teal | \nsystemTeal | \n | \n | \n | \n | \n
\n\n| Blue | \nsystemBlue | \n | \n | \n | \n | \n
\n\n| Indigo | \nsystemIndigo | \n | \n | \n | \n | \n
\n\n| Purple | \nsystemPurple | \n | \n | \n | \n | \n
\n\n| Pink | \nsystemPink | \n | \n | \n | \n | \n
\n\n\n\n| Gray | \nsystemGray | \n | \n | \n | \n | \n
\n\n| Gray (2) | \nsystemGray2 | \n | \n | \n | \n | \n
\n\n| Gray (3) | \nsystemGray3 | \n | \n | \n | \n | \n
\n\n| Gray (4) | \nsystemGray4 | \n | \n | \n | \n | \n
\n\n| Gray (5) | \nsystemGray5 | \n | \n | \n | \n | \n
\n\n| Gray (6) | \nsystemGray6 | \n | \n | \n | \n | \n
\n\n
\nYou may notice that this table doesn’t provide direct correspondences for\nblack or white (or brown, but disregard that for now).
\nBlack and white don’t have adaptive colors\nbecause their names would cease to be descriptive in Dark Mode;\nif .systemBlack existed, it’d pretty much have to be .white\nto be visible in a dark color pallet.
\nWhich gets to a deeper point about color management in an era of Dark Mode…
\n\nUse Semantic Colors
\nThe best way to ensure consistent rendering of your UI\non any device in any appearance mode\nis to use semantic colors,\nnamed according to their function rather than appearance.
\nSimilar to how Dynamic Type\nuses semantic tags like “Headline” and “Body” to\nautomatically set the most suitable font for the user’s display preferences,\nsemantic colors —\nor what Apple’s calling\nUI Element Colors —\nprovide future-proof behavior for your views and controls.
\nWhen styling your component,\nset the color to the closest UIColor class property below:
\n\n
\n- Label Colors
\nlabel \nsecondaryLabel \ntertiaryLabel \nquaternaryLabel \n- Text Colors
\nplaceholderText \n- Link Colors
\nlink \n- Separator Colors
\nseparator \nopaqueSeparator \n- Fill Colors
\nsystemFill \nsecondarySystemFill \ntertiarySystemFill \nquaternarySystemFill \n- Background Colors
\nsystemBackground \nsecondarySystemBackground \ntertiarySystemBackground \n- Grouped Background Colors
\nsystemGroupedBackground \nsecondarySystemGroupedBackground \ntertiarySystemGroupedBackground \n
\n
\n\nUpgrade Homegrown Semantic Color Palettes
\nIf you’ve given any thought to color management in your app\nyou’ll have likely landed on some form of the following strategy,\nwhereby you define semantic colors according to fixed colors\nwithin a namespace or in an extension to UIColor.
\nFor example,\nthe following example shows how an app might define\nUIColor constants from the\nMaterial UI color system\nand reference them from semantic UIColor class properties:
\nimport UIKit\n extension UIColor {\n static var customAccent: UIColor { return MaterialUI.red500 }\n ...\n }\n fileprivate enum MaterialUI {\n static let orange600 = UIColor(red: 0xFB / 0xFF,\n green: 0x8C / 0xFF,\n blue: 0x00 / 0xFF,\n alpha: 1) // #FB8C00\n ...\n }\n
\nIf your app uses a pattern like this,\nyou can make it Dark Mode compatible\nusing the new\ninit(dynamicProvider:)\nUIColor initializer in iOS 13 like so:
\nimport UIKit\n extension UIColor\n static var customAccent: UIColor {\n if #available(iOS 13, *) {\n return UIColor { (traitCollection: UITraitCollection) -> UIColor in\n if traitCollection.userInterfaceStyle == .dark {\n return MaterialUI.orange300\n } else {\n return MaterialUI.orange600\n }\n }\n } else {\n return MaterialUI.orange600\n }\n }\n
\nNothing about the fixed Material UI color constants has to change\nwith this approach.\nInstead, the semantic color property customAccent provides a dynamic color\nthat uses the color most appropriate for the current rendering context.\nWhen Dark Mode is enabled,\na lighter orange is used to contrast against the darker color palette;\notherwise, the behavior is unchanged from the original implementation.
\nThe extra #available check creates some bloat in the implementation,\nbut it’s a small price to pay for the flexibility this approach provides.
\n\nUnfortunately,\nthere’s one crucial shortcoming to using color properties in this way:\nthey can’t be referenced from Interface Builder.
\nIf your app uses either Storyboards or XIBs,\nthe best approach is to use color assets.
\n\nManage Colors with an Asset Catalog
\nColor assets let you manage colors in an Xcode Asset Catalog\nin the same way that you do for\nimages, data, or other resources.
\n\n\n
\n\nTo create a color set,\nopen an Asset Catalog in your Xcode project,\nclick the + button on the bottom left-hand corner,\nand select “New Color Set”.\nIn the Attributes inspector,\nselect “Any, Dark” appearance.\nColors are frequently expressed in the form (“#RRGGBB”);\nyou can enter colors in this form by\nselecting “8-bit Hexadecimal” from the “Input Method” drop-down.
\n\n\n
\n\nHere,\nwe’ve done the same as before,\nexcept instead of defining fixed UIColor constants like orange300 in code,\nwe set those in the color set itself.\nNow when it comes time to reference the color asset\nby the existing semantic class property,\nwe can use the UIColor named initializer:
\nextension UIColor {\n @available(iOS 11, *)\n var customAccent: UIColor! {\n return UIColor(named: "Accent")\n }\n }\n
\n\nYour opinion about color assets will largely be informed by\nyour preference or dispreference towards specialized Xcode document editors.\nSome folks like to have everything spelled out in code,\nwhile others appreciate the affordances provided by a bespoke UI\nlike the one provided by Xcode for color sets.
\nIn fact, your opinion of color assets\nis probably concomitant with your feelings about Storyboards —\nwhich is convenient,\nbecause the killer feature of color assets is that\nthey can be referenced from within Interface Builder.\n(If you’re not on team IB, then you can safely skip this whole discussion.)
\n\nReplace Instances of “Custom Color” in XIBs and Storyboards
\nThe “Custom Color” option in Interface Builder\nthat brings up the macOS system-native color picker\nsuffers the same problem as the color literals and fixed colors\nwe talked about earlier.\nIf you want your XIB-powered views to look good on iOS 13,\nyou’ll need to migrate to named colors.
\nThis can be done easily:\nselect any component in your scene,\nand you can set its color attribute using the same, named color\ndefined in your Asset Catalog.
\n\n\n
\n\nFor a small project,\nthis can be done by hand for all your screens in under an hour.\nHowever,\nfor a larger app,\nthis is a process you’ll want to automate.
\n\nXIB Anatomy
\nUnder the hood,\nXIB and Storyboard files are merely XML files like any other:
\n<?xml version="1.0" encoding="UTF-8"?>\n <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" <#...#>>\n <device id="retina6_1" orientation="portrait">\n <adaptation id="fullscreen"/>\n </device>\n <dependencies>\n <deployment identifier="iOS"/>\n <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>\n <capability name="Safe area layout guides" minToolsVersion="9.0"/>\n <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>\n </dependencies>\n <scenes>\n <!--View Controller-->\n <scene sceneID="nsh-ips-ter">\n <objects>\n <viewController id="dar-kmo-de1" customClass="ViewController" customModule="NSHipster" customModuleProvider="target" sceneMemberID="viewController">\n <view key="view" contentMode="scaleToFill" id="mai-nv-iew">\n <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>\n <color key="backgroundColor" red="1" green="0.69019607843137254" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>\n </view>\n </viewController>\n <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>\n </objects>\n </scene>\n </scenes>\n </document>\n
\nWe wouldn’t want to write this from scratch by hand,\nbut there’s nothing too mysterious going on here.
\nSo consider what happens when you use Xcode\nto switch the custom background color of the main view to a named color:
\n<?xml version="1.0" encoding="UTF-8"?>\n <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" <#...#>>\n <device <#...#>/>\n <dependencies>\n <#...#>\n <capability name="Named colors" minToolsVersion="9.0"/> <!-- ❶ -->\n </dependencies>\n <scenes>\n <!-- scenes.scene.objects.viewController.view -->\n <#...#>\n <view key="view" contentMode="scaleToFill" id="mai-nv-iew">\n <#...#>\n <color key="backgroundColor" name="Accent"/> <!-- ❷ -->\n </view>\n </scenes>\n <resources>\n <namedColor name="Accent"> <!-- ❸ -->\n <color red="1" green="0.69019607843137254" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>\n </namedColor>\n </resources>\n </document>\n
\n\n- ❶
\n- A new
"Named colors" capability is added as a dependency\nfor opening the document\n(Xcode uses this to determine whether it can edit files\ncreated by newer versions). \n- ❷
\n- The
red, green, blue, and alpha components on the color element\nwere replaced by a single name attribute. \n- ❸
\n- A corresponding
namedColor element was added to the top-level resources element. \n
\nBased on this understanding,\nwe should know enough to make this change en masse\nwith our own tooling!
\n\nFinding All Custom Colors
\n\nThe first order of business when migrating your Storyboards and XIBs for Dark Mode\nis to find all of the instances of custom colors.\nYou could go through each by hand and click each of the visual elements…\nor you could run the following command:
\n$ find . -name '*.xib' -or -name '*.storyboard' \\\n -exec echo {} \\; \\\n -exec xmlstarlet sel -t \\\n -m "//color[@colorSpace='custom']" -c . -n {} \\;\n \nMain.storyboard\n<color red="1" green="0.69019607843137254" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>\n
\nThis command prints the name of each file\nfollowed by each custom, unnamed color element it found\n(denoted by the colorSpace="custom" attribute).
\nThe resulting list serves as a battle plan for the next phase of attack:
\n\nFinding Each Distinct Custom Color
\nApps tend to reuse a small set of colors\nacross their views — as they should!\nBy running the following command,\nyou can generate a sorted, unique’d list of\nevery custom color in every XIB or Storyboard:
\n$ find . -name '*.xib' -or -name '*.storyboard' \\\n -exec xmlstarlet sel -t \\\n -m "//color[@colorSpace='custom']" \\\n -v "concat( @red,' ',@green,' ',@blue,' ',@alpha)" -n {} \\; \\\n | sort -u\n \n1 0.69019607839999997 0.0 1\n
\nSome entries may be equivalent, within a small delta of each other\n(because, you know… floating-point numbers).\nTo account for account for this,\nand to transform our output into something easier to work with,\nyou can write the output to a file and process it with\na Ruby script like this one:
\ncolors = File.readlines('distinct_colors.txt').map do |line|\n components = line.strip.split(/\\s+/).flat_map(&:to_f)\n red, green, blue = components[0..2].map{ |c| (c * 255).floor }\n alpha = (components.last * 100).floor\n [red, green, blue, alpha]\n end\n colors.uniq.each do |color|\n puts "#%02X%02X%02X (%d%%)" % color\n end\n
\nFrom here,\nthe final step is to map each set of\nRGBA values\nto the corresponding named color that you want to replace it with.
\n\n\nReplacing Custom Colors
\nAt this point,\nwe’re well beyond the point where shell one-liners seem like a good idea.\nSo here’s a Ruby script we wrote up\nthat makes all of the changes we understand to take place\nwhen replacing a custom color with a named color in Interface Builder:
\n\nrequire 'nokogiri'\n def name_for_rgba_components(red, green, blue, alpha)\n case format('#%02X%02X%02X%02X', red, green, blue, alpha)\n # Return named color matching RGBA components\n # e.g. "#F8CB00FF" => "Accent"\n end\n end\n def name_for_white_and_alpha_components(white, alpha)\n # Return named color matching white and alpha components\n # e.g. 0.9 => "Off-White"\n end\n # Process each Storyboard and XIB file\n Dir['**/*.{storyboard,xib}'].each do |xib|\n doc = Nokogiri::XML(File.read(xib))\n names = []\n # Collect each custom color and assign it a name\n doc.xpath('//objects//color').each do |color|\n next if color['name']\n name = nil\n color_space = color['colorSpace']\n color_space = color['customColorSpace'] if color_space == 'custom'\n case color_space\n when 'sRGB', 'calibratedRGB'\n components = color.attributes\n .values_at('red', 'green', 'blue', 'alpha')\n .map(&:value)\n .map(&:to_f)\n .map { |c| c * 255 }\n name = name_for_rgba_components(*components)\n when 'genericGamma22GrayColorSpace', 'calibratedWhite'\n components = color.attributes\n .values_at('white', 'alpha')\n .map(&:value)\n .map(&:to_f)\n name = name_for_white_and_alpha_components(*components)\n end\n next unless name\n named_color = doc.create_element('color',\n key: color['key'],\n name: name)\n color.replace(named_color)\n names << name\n end\n # Proceed to the next file if no named colors were found\n next if names.empty?\n # Add the named color capability as a document dependency\n dependencies = doc.at('/document/dependencies') ||\n doc.root.add_child(doc.create_element('dependencies'))\n unless dependencies.at("capability[@name='Named colors']")\n dependencies << doc.create_element('capability',\n name: 'Named colors',\n minToolsVersion: '9.0')\n end\n # Add each named color to the document resources\n resources = doc.at('/document/resources') ||\n doc.root.add_child(doc.create_element('resources'))\n names.uniq.sort.each do |name|\n next if resources.at("namedColor[@name='#{name}']")\n resources << doc.create_element('namedColor', name: name)\n end\n # Save the changes\n File.write(xib, doc.to_xml(indent: 4, encoding: 'UTF-8'))\n end\n
\n*Phew!*
\nIf you’ve been facing down a deadline for Dark Mode\nat the expense of enjoying one last hurrah of summer,\nwe hope that this article was able to get you out of the office today.
\nIts ironic that so many of us\nare eschewing our holiday weekend in a scramble to get our apps ready\nfor the annual\nNMOS\nGM.\nBut if it’s any consolation,\nknow that Apple engineers rarely get to observe\nMemorial Day —\nthe unofficial start of summer in America —\nin the run-up to WWDC.
"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/885f2e01-d314-4e63-abac-17dcb063f5b5","label":"Programming"}]},{"keywords":["Swift"],"originId":"https://nshipster.com/identifiable","fingerprint":"ecd98718","id":"08l+9ftpGejQ9f/2DZ6dom5rSnNJJO9OCox6I3nUnWg=_16d689af5ed:4631f:18991ffa","updated":1566802800000,"author":"Mattt","summary":{"direction":"ltr","content":"Swift 5.1 gives us yet another occasion to ponder ontological questions and weigh in the relative merits of various built-in types as stable identifiers.
"},"alternate":[{"href":"https://nshipster.com/identifiable/","type":"text/html"}],"crawled":1569418049005,"title":"Identifiable","published":1566802800000,"origin":{"streamId":"feed/http://nshipster.com/feed.xml","htmlUrl":"https://nshipster.com/","title":"NSHipster"},"content":{"direction":"ltr","content":"\nWhat constitutes the identity of an object?
\n
\nPhilosophers have contemplated such matters throughout the ages.\nWhether it’s to do with\nreconstructed seafaring vessels from antiquity\nor spacefaring vessels from science fiction,\nquestions of Ontology reveal our perception and judgment to be\nmuch less certain than we’d like to believe.
\nOur humble publication has frequented this topic with some regularity,\nwhether it was attempting to make sense of\nequality in Objective-C\nor appreciating the much clearer semantics of Swift\nvis-à-vis the Equatable protocol.
\nSwift 5.1 gives us yet another occasion to ponder this old chestnut\nby virtue of the new Identifiable protocol.\nWe’ll discuss the noumenon of this phenomenal addition to the standard library,\nand help you identify opportunities to\nrealize its potential in your own projects.
\nBut let’s dispense with the navel gazing and\njump right into some substance:
\n
\nSwift 5.1 adds the Identifiable protocol to the standard library,\ndeclared as follows:
\nprotocol Identifiable {\n associatedtype ID: Hashable\n var id: ID { get }\n }\n
\nValues of types adopting the Identifiable protocol\nprovide a stable identifier for the entities they represent.
\nFor example,\na Parcel object may use the id property requirement\nto track the package en route to its final destination.\nNo matter where the package goes,\nit can always be looked up by its id:
\nimport CoreLocation\n struct Parcel: Identifiable {\n let id: String\n var location: CLPlacemark?\n }\n
\n\nThe Swift Evolution proposal for Identifiable,\nSE-0261,\nwas kept small and focused in order to be incorporated quickly.\nSo, if you were to ask,\n“What do you actually get by conforming to Identifiable?”,\nthe answer right now is “Not much.”\nAs mentioned in the future directions,\nconformance to Identifiable has the potential to unlock\nsimpler and/or more optimized versions of other functionality,\nsuch as the new ordered collection diffing APIs.
\nBut the question remains:\n“Why bother conforming to Identifiable?”
\nThe functionality you get from adopting Identifiable is primarily semantic,\nand require some more explanation.\nIt’s sort of like asking,\n“Why bother conforming to Equatable?”
\nAnd actually, that’s not a bad place to start.\nLet’s talk first about Equatable and its relation to Identifiable:
\n\nIdentifiable vs. Equatable
\nIdentifiable distinguishes the identity of an entity from its state.
\nA parcel from our previous example\nwill change locations frequently as it travels to its recipient.\nYet a normal equality check (==)\nwould fail the moment it leaves its sender:
\nextension Parcel: Equatable {}\n var specialDelivery = Parcel(id: "123456789012")\n specialDelivery.location = CLPlacemark(\n location: CLLocation(latitude: 37.3327,\n longitude: -122.0053),\n name: "Cupertino, CA"\n )\n specialDelivery == Parcel(id: "123456789012") // false\n specialDelivery.id == Parcel(id: "123456789012").id // true\n
\nWhile this is an expected outcome from a small, contrived example,\nthe very same behavior can lead to confusing results further down the stack,\nwhere you’re not as clear about how different parts work with one another.
\nvar trackedPackages: Set<Parcel> = ...\n trackedPackages.contains(Parcel(id: "123456789012")) // false (?)\n
\nOn the subject of Set,\nlet’s take a moment to talk about the Hashable protocol.
\n\nIdentifiable vs. Hashable
\nIn our article about Hashable,\nwe described how Set and Dictionary use a calculated hash value\nto provide constant-time (O(1)) access to elements in a collection.\nAlthough the hash value used to bucket collection elements\nmay bear a passing resemblance to identifiers,\nHashable and Identifiable have some important distinctions\nin their underlying semantics:
\n\n- Unlike identifiers,\nhash values are typically state-dependent,\nchanging when an object is mutated.
\n- Identifiers are stable across launches,\nwhereas hash values are calculated by randomly generated hash seeds,\nmaking them unstable between launches.
\n- Identifiers are unique,\nwhereas hash values may collide,\nrequiring additional equality checks when fetched from a collection.
\n- Identifiers can be meaningful,\nwhereas hash values are chaotic\nby virtue of their hashing functions.
\n
\nIn short,\nhash values are similar to\nbut no replacement for identifiers.
\nSo what makes for a good identifier, anyway?
\n\nChoosing ID Types
\nAside from conforming to Hashable,\nIdentifiable doesn’t make any other demands of\nits associated ID type requirement.\nSo what are some good candidates?
\nIf you’re limited to only what’s available in the Swift standard library,\nyour best options are Int and String.\nInclude Foundation,\nand you expand your options with UUID and URL.\nEach has its own strengths and weaknesses as identifiers,\nand can be more or less suited to a particular situation:
\n\nInt as ID
\nThe great thing about using integers as identifiers\nis that (at least on 64-bit systems),\nyou’re unlikely to run out of them anytime soon.
\nMost systems that use integers to identify records\nassign them in an auto-incrementing manner,\nsuch that each new ID is 1 more than the last one.\nHere’s a simple example of how you can do this in Swift:
\nstruct Widget: Identifiable {\n private static var idSequence = sequence(first: 1, next: {$0 + 1})\n let id: Int\n init?() {\n guard let id = Widget.idSequence.next() else { return nil}\n self.id = id\n }\n }\n Widget()?.id // 1\n Widget()?.id // 2\n Widget()?.id // 3\n
\nIf you wanted to guarantee uniqueness across launches,\nyou might instead initialize the sequence with a value\nread from a persistent store like UserDefaults.\nAnd if you found yourself using this pattern extensively,\nyou might consider factoring everything into a self-contained\nproperty wrapper.
\nMonotonically increasing sequences have a lot of benefits,\nand they’re easy to implement.
\nThis kind of approach can provide unique identifiers for records,\nbut only within the scope of the device on which the program is being run\n(and even then, we’re glossing over a lot with respect to concurrency\nand shared mutable state).
\nIf you want to ensure that an identifier is unique across\nevery device that’s running your app, then\ncongratulations —you’ve hit\na fundamental problem in computer science.\nBut before you start in on\nvector clocks and\nconsensus algorithms,\nyou’ll be relieved to know that there’s a\nmuch simpler solution:\nUUIDs.
\n\n\nUUID as ID
\nUUIDs, or\nuniversally unique identifiers,\n(mostly) sidestep the problem of consensus with probability.\nEach UUID stores 128 bits —\nminus 6 or 7 format bits, depending on the\nversion —\nwhich, when randomly generated,\nmake the chances of collision,\nor two UUIDs being generated with the same value,\nastronomically small.
\nAs discussed in a previous article,\nFoundation provides a built-in implementation of (version-4) UUIDs\nby way of the\nUUID type.\nThus making adoption to Identifiable with UUIDs trivial:
\nimport Foundation\n struct Gadget: Identifiable {\n let id = UUID()\n }\n Gadget().id // 584FB4BA-0C1D-4107-9EE5-C555501F2077\n Gadget().id // C9FECDCC-37B3-4AEE-A514-64F9F53E74BA\n
\nBeyond minor ergonomic and cosmetic issues,\nUUID serves as an excellent alternative to Int\nfor generated identifiers.
\nHowever,\nyour model may already be uniquely identified by a value,\nthereby obviating the need to generate a new one.\nUnder such circumstances,\nthat value is likely to be a String.
\n\n\nString as ID
\nWe use strings as identifiers all the time,\nwhether it takes the form of a username or a checksum or a translation key\nor something else entirely.
\nThe main drawback to this approach is that,\nthanks to The Unicode® Standard,\nstrings encode thousands of years of written human communication.\nSo you’ll need a strategy for handling identifiers like\n“⽜”, “\uD800\uDC8C”, “”, and “\uD83D\uDC2E”\n…and that’s to say nothing of the more pedestrian concerns,\nlike leading and trailing whitespace and case-sensitivity!
\nNormalization is the key to successfully using strings as identifiers.\nThe easiest place to do this is in the initializer,\nbut, again, if you find yourself repeating this code over and over,\nproperty wrappers can help you here, too.
\nimport Foundation\n fileprivate extension String {\n var nonEmpty: String? { isEmpty ? nil : self }\n }\n struct Whosit: Identifiable {\n let id: String\n init?(id: String) {\n guard let id = id.trimmingCharacters(in: CharacterSet.letters.inverted)\n .lowercased()\n .nonEmpty\n else {\n return nil\n }\n self.id = id\n }\n }\n Whosit(id: "Cow")?.id // cow\n Whosit(id: "--- cow ---")?.id // cow\n Whosit(id: "\uD83D\uDC2E") // nil\n
\n\nURL as ID
\nURLs (or URIs if you want to be pedantic)\nare arguably the most ubiquitous kind of identifier\namong all of the ones described in this article.\nEvery day, billions of people around the world use URLs\nas a way to point to a particular part of the internet.\nSo URLs a natural choice for an id value\nif your models already include them.
\nURLs look like strings,\nbut they use syntax\nto encode multiple components,\nlike scheme, authority, path, query, and fragment.\nAlthough these formatting rules dispense with much of the invalid input\nyou might otherwise have to consider for strings,\nthey still share many of their complexities —\nwith a few new ones, just for fun.
\nThe essential problem is that\nequivalent URLs may not be equal.\nIntrinsic, syntactic details like\ncase sensitivity,\nthe presence or absence of a trailing slash (/),\nand the order of query components\nall affect equality comparison.\nSo do extrinsic, semantic concerns like\na server’s policy to upgrade http to https,\nredirect from www to the apex domain,\nor replace an IP address with a\nwhich might cause different URLs to resolve to the same webpage.
\nURL(string: "https://nshipster.com/?a=1&b=2")! ==\n URL(string: "http://www.NSHipster.com?b=2&a=1")! // false\n try! Data(contentsOf: URL(string: "https://nshipster.com?a=1&b=2")!) ==\n Data(contentsOf: URL(string: "http://www.NSHipster.com?b=2&a=1")!) // true\n
\n\nIf your model gets identifier URLs for records from a trusted source,\nthen you may take URL equality as an article of faith;\nif you regard the server as the ultimate source of truth,\nit’s often best to follow their lead.
\nBut if you’re working with URLs in any other capacity,\nyou’ll want to employ some combination of\nURL normalizations\nbefore using them as an identifier.
\nUnfortunately, the Foundation framework doesn’t provide\na single, suitable API for URL canonicalization,\nbut URL and URLComponents provide enough on their own\nto let you roll your own\n(though we’ll leave that as an exercise for the reader):
\nimport Foundation\n fileprivate extension URL {\n var normalizedString: String { ... }\n }\n struct Whatsit: Identifiable {\n let url: URL\n var id: { url.normalizedString }\n }\n Whatsit(url: "https://example.com/123").id // example.com/123\n Whatsit(id: "http://Example.com/123/").id // example.com/123\n
\n\nCreating Custom Identifier ID Types
\nUUID and URL both look like strings,\nbut they use syntax rules to encode information in a structured way.\nAnd depending on your app’s particular domain,\nyou may find other structured data types that\nwould make for a suitable identifier.
\nThanks to the flexible design of the Identifiable protocol,\nthere’s nothing to stop you from implementing your own ID type.
\nFor example,\nif you’re working in a retail space,\nyou might create or repurpose an existing\nUPC type\nto serve as an identifier:
\nstruct UPC: Hashable {\n var digits: String\n implementation details\n }\n struct Product: Identifiable {\n let id: UPC\n var name: String\n var price: Decimal\n }\n
\n\nThree Forms of ID Requirements
\nAs Identifiable makes its way into codebases,\nyou’re likely to see it used in one of three different ways:
\nThe newer the code,\nthe more likely it will be for id to be a stored property —\nmost often this will be declared as a constant (that is, with let):
\nimport Foundation\n // Style 1: id requirement fulfilled by stored property\n struct Product: Identifiable {\n let id: UUID\n }\n
\nOlder code that adopts Identifiable,\nby contrast,\nwill most likely satisfy the id requirement\nwith a computed property\nthat returns an existing value to serve as a stable identifier.\nIn this way,\nconformance to the new protocol is purely additive,\nand can be done in an extension:
\nimport Foundation\n struct Product {\n var uuid: UUID\n }\n // Style 2: id requirement fulfilled by computed property\n extension Product: Identifiable {\n var id { uuid }\n }\n
\nIf by coincidence the existing class or structure already has an id property,\nit can add conformance by simply declaring it in an extension\n(assuming that the property type conforms to Hashable).
\nimport Foundation\n struct Product {\n var id: UUID\n }\n // Style 3: id requirement fulfilled by existing property\n extension Product: Identifiable {}\n
\nNo matter which way you choose,\nyou should find adopting Identifiable in a new or existing codebase\nto be straightforward and noninvasive.
\n
\nAs we’ve said time and again,\noften it’s the smallest additions to the language and standard library\nthat have the biggest impact on how we write code.\n(This speaks to the thoughtful,\nprotocol-oriented\ndesign of Swift’s standard library.)
\nBecause what Identifiable does is kind of amazing:\nit extends reference semantics to value types.
\nWhen you think about it,\nreference types and value types differ not in what information they encode,\nbut rather how we treat them.
\nFor reference types,\nthe stable identifier is the address in memory\nin which the object resides.\nThis fact can be plainly observed\nby the default protocol implementation of id for AnyObject types:
\nextension Identifiable where Self: AnyObject {\n var id: ObjectIdentifier {\n return ObjectIdentifier(self)\n }\n }\n
\nEver since Swift first came onto the scene,\nthe popular fashion has been to eschew all reference types for value types.\nAnd this neophilic tendency has only intensified\nwith the announcement of SwiftUI.\nBut taking such a hard-line approach makes a value judgment\nof something better understood to be a difference in outlook.
\nIt’s no coincidence that much of the terminology of programming\nis shared by mathematics and philosophy.\nAs developers, our work is to construct logical universes, after all.\nAnd in doing so,\nwe’re regularly tasked with reconciling our own mental models\nagainst that of every other abstraction we encounter down the stack —\ndown to the very way that we understand electricity and magnetism to work.
"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/885f2e01-d314-4e63-abac-17dcb063f5b5","label":"Programming"}]},{"id":"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d68670508:1d8c:90d684ff","originId":"https://www.raywenderlich.com/9527-functional-programming-with-kotlin-and-arrow-getting-started","fingerprint":"5064fed4","title":"Functional Programming with Kotlin and Arrow: Getting Started [FREE]","updated":1569414645000,"summary":{"content":"In this tutorial, you will learn the fundamentals of functional programming and how various Kotlin language features enable functional programming concepts.","direction":"ltr"},"alternate":[{"href":"https://www.raywenderlich.com/9527-functional-programming-with-kotlin-and-arrow-getting-started","type":"text/html"}],"crawled":1569414645000,"published":1569414645000,"origin":{"streamId":"feed/http://www.raywenderlich.com/feed","title":"Ray Wenderlich | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more","htmlUrl":"http://www.raywenderlich.com/feed"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/885f2e01-d314-4e63-abac-17dcb063f5b5","label":"Programming"}]},{"id":"RFlzskW4NhJjlZfijOSI8IXqM9+zz6V9qnDVl1gxaJs=_16d635b7080:1d8b:90d684ff","originId":"https://www.raywenderlich.com/5539282-programming-in-swift-fundamentals","fingerprint":"910498d4","title":"Programming in Swift: Fundamentals [SUBSCRIBER]","updated":1569330000000,"summary":{"content":"Learn about Apple’s open source programming language, Swift, through hands-on examples! Take a deep dive into the Swift language, learning about core Swift concepts like loops, collections, types, optionals, functions, classes, and more.","direction":"ltr"},"alternate":[{"href":"https://www.raywenderlich.com/5539282-programming-in-swift-fundamentals","type":"text/html"}],"crawled":1569330000000,"published":1569330000000,"origin":{"streamId":"feed/http://www.raywenderlich.com/feed","title":"Ray Wenderlich | High quality programming tutorials: iOS, Android, Swift, Kotlin, Unity, and more","htmlUrl":"http://www.raywenderlich.com/feed"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/885f2e01-d314-4e63-abac-17dcb063f5b5","label":"Programming"}]}]}
\ No newline at end of file
diff --git a/Frameworks/Account/AccountTests/Feedly/Initial/saved_initial.json b/Frameworks/Account/AccountTests/Feedly/Initial/saved_initial.json
deleted file mode 100644
index 497f76673..000000000
--- a/Frameworks/Account/AccountTests/Feedly/Initial/saved_initial.json
+++ /dev/null
@@ -1 +0,0 @@
-{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.saved","items":[{"originId":"https://inessential.com/2019/10/14/netnewswire_os_compatibility_strategy","fingerprint":"fa6e6fff","id":"+jHfsXnBCVfCstSIW1WDumAyigT4rnsUPnI5WFxgnAU=_16dccdfc972:a6e5:d4506071","summary":{"direction":"ltr","content":"We have two goals with the app: 1) get as many people using RSS as possible, and 2) make the best app we can.
\nTo reach #2 — making the best app we can — we need to do a couple things. One is stay modern: use new APIs and tools that make the app better and easier to maintain. A second is to not spend time on things that don’t make the app better. A third is to attract and retain contributors, who are usually more psyched to work with modern stuff than with old stuff.
\nYou can see how that’s in a little bit of conflict with #1 (getting as many people as possible using RSS readers).
\nHere’s the plan
\nAfter a major OS update, we will switch to requiring that update on our next major release — where major is defined as something like 5.0 or 5.1, but not something like 5.0.1. (In other words: the upcoming NetNewsWire 5.0.3 release will run on Mojave, while NetNewsWire 5.1 will require Catalina.)
\nAt the same time, we will make older versions available via the website. For instance, the last version that will run on Mojave will likely be 5.0.4 (which isn’t finished yet) — and we’ll make that version available indefinitely for people who haven’t upgraded to Catalina.
\nThis will mean that people running older OSes will still get a high-quality app — it’s just that it won’t have the latest features.
\nThe key is that this allows us to make NetNewsWire the best app it can be, and making the best app we can is also part of furthering the goal of getting as many people as possible using RSS. (The biggest part, in fact. Bigger than compatibility with older OSes.)
\nWhile I know this will disappoint some people, I hope you’ll understand why we decided to do it this way. Decisions like this are never easy — there are always conflicting values to weigh, pros and cons and add up — and we don’t make them impulsively. But making NetNewsWire the best app it can be has to be job #1.
"},"alternate":[{"href":"https://inessential.com/2019/10/14/netnewswire_os_compatibility_strategy","type":"text/html"}],"crawled":1571100281202,"title":"NetNewsWire OS Compatibility Strategy","published":1571099404000,"origin":{"streamId":"feed/http://ranchero.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":false,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8","label":"THree"}],"tags":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.saved","label":"Saved For Later"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.read","label":""}],"actionTimestamp":1571123677415},{"keywords":["Xcode"],"originId":"https://nshipster.com/swiftui-previews","fingerprint":"e3a6f78b","id":"08l+9ftpGejQ9f/2DZ6dom5rSnNJJO9OCox6I3nUnWg=_16dc8d7749a:96ed:d4506071","updated":1571036400000,"author":"Mattt","summary":{"direction":"ltr","content":"Working on a large iOS codebase often involves a lot of waiting. But with Xcode 11, our wait is finally over — and it’s all thanks to SwiftUI.
"},"alternate":[{"href":"https://nshipster.com/swiftui-previews/","type":"text/html"}],"crawled":1571032626330,"title":"SwiftUI Previews on macOS Catalina and Xcode 11","published":1571036400000,"origin":{"streamId":"feed/http://nshipster.com/feed.xml","htmlUrl":"https://nshipster.com/","title":"NSHipster"},"content":{"direction":"ltr","content":"Working on a large iOS codebase often involves a lot of waiting:\nWaiting for Xcode to index your files,\nwaiting for Swift and Objective-C code to compile,\nwaiting for the Simulator to boot and your app to launch…
\nAnd after all of that,\nyou spend even more time getting your app\ninto a particular state and onto a particular screen,\njust to see whether the Auto Layout constraint you just added\nfixes that regression you found.\nIt didn’t, of course,\nso you jump back into Xcode,\ntweak the Content Hugging Priority,\nhit ⌘R,\nand start the whole process again.
\nWe might relate our sorry predicament to\nthat one xkcd comic,\nbut for those of us who don’t so much relish in\nthe stop-and-go nature of app development,\nthere’s an old Yiddish joke about Shlemiel the painter\n(provided below with a few -specific modifications;\nfor the uninitiated,\nplease refer to Joel Spolsky’s\noriginal telling):
\n\nShlemiel gets a job as a software developer,\nimplementing a new iOS app.\nOn the first sprint he opens Xcode\nand implements 10 new screens of the app.\n“That’s pretty good!” says his manager,\n“you’re a fast worker!” and pays him a Bitcoin.
\nThe next sprint Shlemiel only gets 5 screens done.\n“Well, that’s not nearly as good as yesterday,\nbut you’re still a fast worker. 5 screens is respectable,”\nand pays him a Bitcoin.
\nThe next sprint Shlemiel implements 1 screen.\n“Only 1!” shouts his manager.\n“That’s unacceptable!\nOn the first day you did ten times that much work!\nWhat’s going on?”
\n“I can’t help it,” says Shlemiel.\n“Each sprint I get further and further away from\napplication(_:didFinishLaunchingWithOptions:)!”
\n
\nOver the years,\nthere have been some developments that’ve helped things slightly,\nincluding\n@IBInspectable and @IBDesignable\nand Xcode Playgrounds.\nBut with Xcode 11,\nour wait is finally over —\nand it’s all thanks to SwiftUI.
\n
\n\n
\nAlthough many of us have taken a “wait and see” approach to SwiftUI,\nwe can start using its capabilities today\nto radically speed up and improve our development process —\nwithout changing a line of code in our UIKit apps.
\nConsider a subclass of UIButton\nthat draws a border around itself:
\nfinal class BorderedButton: UIButton {\n var cornerRadius: CGFloat { ... }\n var borderWidth: CGFloat { ... }\n var borderColor: UIColor? { ... }\n }\n
\nNormally,\nif we wanted to test how our UI element performs,\nwe’d have to add it to a view in our app,\nbuild and run,\nand navigate to that screen.\nBut with Xcode 11,\nwe can now see a preview side-by-side with the code editor\nby adding the following under the original declaration of BorderedButton:
\n\n
#if canImport(SwiftUI) && DEBUG\n import SwiftUI\n @available(iOS 13.0, *)\n struct BorderedButton_Preview: PreviewProvider {\n static var previews: some View {\n UIViewPreview {\n let button = BorderedButton(frame: .zero)\n button.setTitle("Follow", for: .normal)\n button.tintColor = .systemOrange\n button.setTitleColor(.systemOrange, for: .normal)\n return button\n }.previewLayout(.sizeThatFits)\n .padding(10)\n }\n }\n #endif\n
\n
\n
\nUsing a new feature called dynamic replacement,\nXcode can update this preview without recompiling —\nwithin moments of your making a code change.\nThis lets you rapidly prototype changes like never before.
\nWant to see how your button handles long titles?\nBang away on your keyboard within the call to setTitle(_:for:)\nin your preview,\nand test out potential fixes in your underlying implementation\nwithout so much as leaving your current file!
\n\n\nPreviewing Multiple States
\nLet’s say our app had a FavoriteButton —\na distant cousin (perhaps by composition) to BorderedButton.\nIn its default state,\nit shows has the title “Favorite”\nand displays a ♡ icon.\nWhen its isFavorited property is set to true,\nthe title is set to “Unfavorite”\nand displays a ♡̸ icon.
\nWe can preview both at once\nby wrapping two UIViewPreview instances within a single SwiftUI Group:
\n\n
Group {\n UIViewPreview {\n let button = FavoriteButton(frame: .zero)\n return button\n }\n UIViewPreview {\n let button = FavoriteButton(frame: .zero)\n button.isFavorited = true\n return button\n }\n }.previewLayout(.sizeThatFits)\n .padding(10)\n
\n
\n
\n\n\nPreviewing Dark Mode
\nWith Dark Mode in iOS 13,\nit’s always a good idea to double-check that your custom views\nare configured with dynamic colors\nor accommodate both light and dark appearance in some other way.
\nAn easy way to do this\nwould be to use a ForEach element\nto render a preview for each case in the ColorScheme enumeration:
\n\n
ForEach(ColorScheme.allCases, id: \\.self) { colorScheme in\n UIViewPreview {\n let button = BorderedButton(frame: .zero)\n button.setTitle("Subscribe", for: .normal)\n button.setImage(UIImage(systemName: "plus"), for: .normal)\n button.setTitleColor(.systemOrange, for: .normal)\n button.tintColor = .systemOrange\n return button\n }.environment(\\.colorScheme, colorScheme)\n .previewDisplayName("\\(colorScheme)")\n }.previewLayout(.sizeThatFits)\n .background(Color(.systemBackground))\n .padding(10)\n
\n
\n
\n\n\nPreviewing Dynamic Type Size Categories
\nWe can use the same approach to preview our views in various\nDynamic Type Sizes:
\n\n
ForEach(ContentSizeCategory.allCases, id: \\.self) { sizeCategory in\n UIViewPreview {\n let button = BorderedButton(frame: .zero)\n button.setTitle("Subscribe", for: .normal)\n button.setImage(UIImage(systemName: "plus"), for: .normal)\n button.setTitleColor(.systemOrange, for: .normal)\n button.tintColor = .systemOrange\n return button\n }.environment(\\.sizeCategory, sizeCategory)\n .previewDisplayName("\\(sizeCategory)")\n }.previewLayout(.sizeThatFits)\n .padding(10)\n
\n
\n
\n\nPreviewing Different Locales
\nXcode Previews are especially time-saving when it comes to\nlocalizing an app into multiple languages.\nCompared to the hassle of configuring Simulator\nback and forth between different languages and regions,\nthis new approach makes a world of difference.
\nLet’s say that, in addition to English,\nyour app supported various right-to-left languages.\nYou could verify that your\nRTL logic worked as expected like so:
\n\n
let supportedLocales: [Locale] = [\n "en-US", // English (United States)\n "ar-QA", // Arabid (Qatar)\n "he-IL", // Hebrew (Israel)\n "ur-IN" // Urdu (India)\n ].map(Locale.init(identifier:))\n func localizedString(_ key: String, for locale: Locale) -> String? { ... }\n return ForEach(supportedLocales, id: \\.identifier) { locale in\n UIViewPreview {\n let button = BorderedButton(frame: .zero)\n button.setTitle(localizedString("Subscribe", for: locale), for: .normal)\n button.setImage(UIImage(systemName: "plus"), for: .normal)\n button.setTitleColor(.systemOrange, for: .normal)\n button.tintColor = .systemOrange\n return button\n }.environment(\\.locale, locale)\n .previewDisplayName(Locale.current.localizedString(forIdentifier: locale.identifier))\n }.previewLayout(.sizeThatFits)\n .padding(10)\n
\n
\n
\n\n\nPreviewing View Controllers on Different Devices
\nSwiftUI previews aren’t limited to views,\nyou can also use them with view controllers.\nBy creating a custom UIViewControllerPreview type\nand taking advantage of some\nnew UIStoryboard class methods in iOS 13,\nwe can easily preview our view controller\non various devices —\none on top of another:
\n\n
#if canImport(SwiftUI) && DEBUG\n import SwiftUI\n let deviceNames: [String] = [\n "iPhone SE",\n "iPad 11 Pro Max",\n "iPad Pro (11-inch)"\n ]\n @available(iOS 13.0, *)\n struct ViewController_Preview: PreviewProvider {\n static var previews: some View {\n ForEach(deviceNames, id: \\.self) { deviceName in\n UIViewControllerPreview {\n UIStoryboard(name: "Main", bundle: nil)\n .instantiateInitialViewController { coder in\n ViewController(coder: coder)\n }!\n }.previewDevice(PreviewDevice(rawValue: deviceName))\n .previewDisplayName(deviceName)\n }\n }\n }\n #endif\n
\n
\n
\n\n
\nAlthough most of us are still some years away from shipping SwiftUI in our apps\n(whether by choice or necessity),\nwe can all immediately benefit from the order-of-magnitude improvement\nit enables with Xcode 11 on macOS Catalina.
\nBy eliminating so much time spent waiting for things to happen,\nwe not only get (literally) hours more time each week,\nbut we unlock the possibility of maintaining an unbroken flow state during that time.\nNot only that,\nbut the convenience of integrated tests\nfundamentally changes the calculus for testing:\ninstead of being a rare “nice to have,”\nthey’re the new default.\nPlus:\nthese inline previews serve as living documentation\nthat can help teams both large and small\nfinally get a handle on their design system.
\nIt’s hard to overstate how much of a game-changer Xcode Previews are for iOS development,\nand we couldn’t be happier to incorporate them into our workflow.
\n"},"visual":{"url":"none"},"unread":false,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/885f2e01-d314-4e63-abac-17dcb063f5b5","label":"Programming"}],"tags":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.saved","label":"Saved For Later"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.read","label":""}],"actionTimestamp":1571037592868},{"originId":"tag:blogger.com,1999:blog-8954608646904080796.post-3215871338266756283","fingerprint":"717870dc","thumbnail":[{"url":"https://1.bp.blogspot.com/-JTONCNpv2X8/XaEUgnGZSzI/AAAAAAAADTg/EaM5cDjkD3kRnXPIR0-6AX-3VxlC_br_QCEwYBhgL/s72-c/091.jpg","width":72,"height":72}],"id":"v0v+7Ya8tssIZvd3/pcnFRr3HwvY/5YK3FGc2t65c0Y=_16dbd619e56:82fa:d4506071","updated":1570837682218,"author":"Edward Feser","alternate":[{"href":"http://edwardfeser.blogspot.com/2019/10/around-web.html","type":"text/html"}],"crawled":1570840354390,"title":"Around the web","published":1570837680000,"origin":{"streamId":"feed/http://edwardfeser.blogspot.com/feeds/posts/default","htmlUrl":"http://edwardfeser.blogspot.com/","title":"Edward Feser"},"content":{"direction":"ltr","content":"
"},"visual":{"url":"http://b.vimeocdn.com/ts/452/218/452218069_1280.jpg","width":1280,"height":720,"contentType":"image/jpeg"},"unread":false,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/5ca4d61d-e55d-4999-a8d1-c3b9d8789815","label":"Macintosh"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fbdcd69b-7e27-4b6a-bfed-6584b944155d","label":"🤞🏻🤞🏻🤞🏻"}],"tags":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.saved","label":"Saved For Later"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.read","label":""}],"actionTimestamp":1571037184309},{"keywords":["Google","Apple Arcade"],"originId":"https://www.macrumors.com/2019/09/26/apple-arcade-vs-google-play-pass/","fingerprint":"fd93a55d","id":"SusR11hdg5ydv7o+xGl+0XlI0AhBL77Oxid8QOO3j5k=_16d6f7df794:48e16:18991ffa","author":"Juli Clover","summary":{"direction":"ltr","content":"With the launch of iOS 13, Apple released Apple Arcade, a new $4.99 per month gaming service that provides unlimited access to new and exclusive games.\n
\n
\nLess than two weeks later, Google announced its own gaming service called Play Pass, which also offers unlimited access to games. In our latest YouTube video, we went hands-on with both services to compare them.\n
\n
\nSubscribe to the MacRumors YouTube channel for more videos.\n
\nBoth Apple Arcade and Play Pass are priced at $4.99 per month. Apple offers a one-month free trial while Google offers a 10-day free trial, but for the first year, Google is offering a deal that drops the price of Play Pass to $1.99 per month.\n
\n
\nThough the prices are similar, the two services are quite different. Apple Arcade features new and exclusive games, some of which were funded by Apple, while Google's Play Pass offers up older games.\n
\n
\nPlay Pass includes some super popular titles like Stardew Valley, Reigns, Star Wars Knights of the Old Republic, Terraria, and more, but the problem with older games is that many people may have already played them.\n
\n
\nPlay Pass is also not limited to games -- Google is including apps too. AccuWeather, Pic Stitch, ISS HD, and Tunable are some of the apps offered.\n
\n
\nApple Arcade only offers gaming titles, but all of the games are fresh, new, and exclusive to Apple Arcade. In some cases, though, some of the games are also available on consoles, but on mobile platforms, Apple Arcade gets exclusive access. That means no Android equivalent.\n
\n
\nIt's not clear if apps can leave Apple Arcade, but Google warns that apps can leave Play Pass. If that happens, users will need to buy the game to continue to use it if it's a paid game, and for free titles, ads and in-app purchases may show up.\n
\n
\nGoogle says there are hundreds of apps and games included with more being added every month. Apple Arcade launched with right around 60 games, and Apple has also promised new content on a monthly basis. Both services offer offline gaming, so no internet connection is required.\n
\n
\nWith both services, content is ad free and features no in-app purchases. Apple titles were designed from the ground up with no additional purchases, but for Play Pass, these gaming elements have been removed from titles that previously offered them.\n
\n
\nApple allows up to six family members to share games through a single Apple Arcade subscription using Family Sharing, and Google allows for up to five family members to share content through its Google Play Family Library.\n
\n
\nApple Arcade has launched in multiple countries around the world, while Play Pass is limited to the United States at the current time. Google does plan to expand, however.\n
\n
\nOne other aspect worth noting is privacy. Apple specifically mentions privacy protections and says that users are able to choose to share data, while Google's Play Pass materials don't mention privacy or data sharing. "Every game must meet Apple's high privacy standards," reads Apple's press release for Apple Arcade.\n
\n
\nMost people are locked in to either Android or iOS and few have both, so most people won't need to choose between services.\n
\n
\nApple users who have a Mac, iPhone, iPad, or Apple TV can access Apple Arcade on those devices, while Google Play Pass requires a smartphone, laptop, or tablet with Android 4.4 or above and Play Store version 16.6.25. Google Play Pass games can't be played on the larger screen of a television, which gives Apple Arcade a bit of an edge when it comes to non-mobile gaming.\n
\n
\nDo you prefer Apple Arcade with its fresh selection of titles, or Google Play Pass with its larger library of already available games? Let us know in the comments.
This article, "Apple Arcade vs. Google Play Pass" first appeared on MacRumors.com
Discuss this article in our forums
"},"alternate":[{"href":"https://www.macrumors.com/2019/09/26/apple-arcade-vs-google-play-pass/","type":"text/html"}],"crawled":1569533589396,"title":"Apple Arcade vs. Google Play Pass","published":1569531802000,"origin":{"streamId":"feed/http://www.macrumors.com/macrumors.xml","htmlUrl":"https://www.macrumors.com","title":"MacRumors: Mac News and Rumors - Front Page"},"visual":{"url":"https://cdn.vox-cdn.com/thumbor/m3ztxUEeETYXD9gGfyEjUpS0GFE=/0x0:800x533/1310x873/cdn.vox-cdn.com/uploads/chorus_image/image/59662701/800x_1.0.jpg","width":1310,"height":873,"contentType":"image/jpeg"},"unread":false,"readTime":33744,"tags":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.read","label":""},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.unsaved","label":"Unsaved"},{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/tag/global.saved","label":"Saved For Later"}],"actionTimestamp":1569536278861}]}
\ No newline at end of file
diff --git a/Frameworks/Account/AccountTests/Feedly/Initial/uncategorized_initial.json b/Frameworks/Account/AccountTests/Feedly/Initial/uncategorized_initial.json
deleted file mode 100644
index 017803b50..000000000
--- a/Frameworks/Account/AccountTests/Feedly/Initial/uncategorized_initial.json
+++ /dev/null
@@ -1 +0,0 @@
-{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8","updated":1569829929101,"continuation":"16d8125cccf:1a5d:90d684ff","items":[{"keywords":["Philosophy"],"originId":"https://www.natashatherobot.com/?p=6640","fingerprint":"ebbe9c9f","id":"/3LYgCDhmnhJO0J4u5kYndnXcLTv6D7IYLzm+0z9WVA=_16d8127c08d:4d99b:18991ffa","author":"Natasha Murashev","summary":{"direction":"ltr","content":"Lately, I've been thinking a lot about functional programming... in relation to life."},"alternate":[{"href":"https://www.natashatherobot.com/implementation-matters/","type":"text/html"}],"crawled":1569829929101,"title":"Implementation Matters","published":1545498499000,"origin":{"streamId":"feed/http://natashatherobot.com/feed/","htmlUrl":"https://www.natashatherobot.com","title":"Natasha The Robot"},"content":{"direction":"ltr","content":"Lately, I’ve been thinking a lot about functional programming… in relation to life. While there are lots of complicated and intimidating concepts that are included as part of functional programming, on the base level, functional programming is super simple.
\nIt’s about cleanly transforming a set of inputs into outputs, without the side-effects of changing-state and mutable data. In functional programming, the implementation of the function almost doesn’t matter. The important thing is that it takes in a set of inputs, it transforms them through the implementation, and gives back certain outputs.
\nBut if you really think about the process of transformation, the break down of the original ingredients to create something new, there will be some type of leftover in the transformation process. When you burn wood, you transform it into a fire/warmth, but you also have leftover ash. Your function will take wood as the input and output the warmth, but the implementation will have some leftover ash to it.
\nYour clean mathematical function will have something left in it. Something very subtle and intangible compared to the output. Some type of energy, an essense perhaps. As the law of physics goes, energy cannot be created or destroyed.
\nWhy is it that there could be two apps on the market that do exactly the same thing (the same output), but one app will have millions of users while the other will have none. I think the answer is in that leftover implementation detail. The successful app was made with love and passion, while the unsuccessful app was made simply to make money. The byproduct of the successful app is love while the byproduct of the unsuccessful app is greed.
\nThe consumer can feel this and is attracted by that subtle love byproduct.
\nAnother real-world example is my grandmother’s banana bread. She makes the best banana bread! Let’s say I ask my grandma for the banana bread recipe and she gives it to me. I will go into my kitchen, follow the recipe exactly, but no matter how much I try, my banana bread will never be the same as my grandmothers. Why? Because my grandmother takes the ingredients and implements their transformation in a way that creates deep love as the byproduct. So while the inputs and outputs of my grandmothers and my banana bread are exactly the same, my implementation creates a different byproduct than my grandmothers.
\nThe implementation really does matter. In fact, I would argue that its subtle byproduct is almost more important than the pure output. It gives essence, a soul, to the output. That essence could be good or bad energy.
\nWith this in mind and the end of the year coming up fast, I’d like to challenge you to think a little differently about New Year’s resolutions. Instead of coming up with new things you’re going to do, consider analyzing the implementation byproducts of the things you’re already doing every day and transforming those if needed.
\nYou might already go to the gym. But are you going to the gym because you hate how your body looks or are you going to the gym because you respect your body as a container for your soul and it’s important for you to keep that container healthy? To the outside world, the output will be the same – you already go to the gym and you will continue going to the gym, but when you change the implementation byproduct from hate for your body (not being pretty or good enough) to love for your body (it’s a vessel for your supreme being), you and everyone around you will feel the difference on that subtle level.
\nHow are you implementing your Github comments at work? Are you communicating with judgment and annoyance (especially at that new intern) or are you commenting with the byproduct of compassion and spirit of teaching?
\nIf you already have a blog, does your implementation stem from a desire for fame and recognition or out of love of sharing what you’re learning?
\nThe byproduct of nature is always love. It keeps creating and nourishing us no matter how bad we keep treating it. We trample the earth with our feet every day, but it still gives us fruit. I wish you all the byproduct of love and compassion in everything you do in the coming year.
\nHappy Holidays!
\nNatasha
"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8","label":"Uncategorized"}]},{"id":"pnMbeb2l5zEGNlmI1nQ7v6sNtbK5C35P3jb9CziBeoQ=_16d8126dc16:1c07:90d684ff","originId":"https://pewpewthespells.com//blog/website-update-2018.html","fingerprint":"8e21a7fc","title":"Website Stack Update","summary":{"content":"Some info about updates to the website","direction":"ltr"},"alternate":[{"href":"https://pewpewthespells.com//blog/website-update-2018.html","type":"text/html"}],"crawled":1569829870614,"published":1569829870614,"origin":{"streamId":"feed/http://pewpewthespells.com/feed.xml","title":"Samantha Demi's Blog","htmlUrl":"https://pewpewthespells.com/"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8","label":"Uncategorized"}]},{"originId":"https://inessential.com/2019/09/25/i_had_the_fun_of_interviewing_old_friend","fingerprint":"efab5851","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d996:18991ffa","summary":{"direction":"ltr","content":"I had the fun of interviewing old friend Daniel Jalkut on the latest episode of The Omni Show.
"},"alternate":[{"href":"https://inessential.com/2019/09/25/i_had_the_fun_of_interviewing_old_friend","type":"text/html"}],"crawled":1569829821629,"published":1569437386000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8","label":"Uncategorized"}]},{"originId":"https://inessential.com/2019/09/13/netnewswire_5_0_1_released","fingerprint":"f53acc86","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d995:18991ffa","summary":{"direction":"ltr","content":"
\nNetNewsWire 5.0.1 is almost entirely a bug-fix release — see the release notes for the full scoop.
\nIt includes one sort-of new feature: there’s now a checkbox in Preferences for turning off the unread count in the Dock. (It was a hidden pref — now it’s visible.)
\nStatus
\nHere’s what else we’re working on:
\n\n- iOS/iPadOS app
\n- NetNewsWire 5.0.2 for Mac — which will mainly be about performance (yes, we can make it even faster)
\n- NetNewsWire 5.1 for Mac — tentative feature list includes content extraction and at least one more syncing option (but we might change our minds on these: anything can happen between now and then)
\n
\nWe might also distribute NetNewsWire 5.0.2 for Mac on the Mac App Store. No guarantees yet, of course, but work is happening in that direction. This goes to our goal of getting as many people as possible using RSS readers.
"},"alternate":[{"href":"https://inessential.com/2019/09/13/netnewswire_5_0_1_released","type":"text/html"}],"crawled":1569829821629,"title":"NetNewsWire 5.0.1 Released","published":1568408217000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8","label":"Uncategorized"}]},{"originId":"https://inessential.com/2019/09/10/had_to_get_a_new_key_fob_at_work_today_m","fingerprint":"5b6c292f","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d994:18991ffa","summary":{"direction":"ltr","content":"Had to get a new key fob at work today — my old one wore out. Just a couple weeks shy of my fifth anniversary at Omni! Time flies.
\nI figure I’m just over eight years from retiring, so I’m not even halfway done here. :)
"},"alternate":[{"href":"https://inessential.com/2019/09/10/had_to_get_a_new_key_fob_at_work_today_m","type":"text/html"}],"crawled":1569829821629,"published":1568153137000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8","label":"Uncategorized"}]},{"originId":"https://inessential.com/2019/09/06/on_syncing_netnewswire_using_icloud","fingerprint":"3b5ade1b","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d993:18991ffa","summary":{"direction":"ltr","content":"People have been asking me about supporting iCloud as a sync method for NetNewsWire.
\nIt would be really cool because:
\n\n- There’s no sign-in
\n- It’s free — no need to spend money on another service
\n- It would help broaden the pool of people using RSS, since there would be no additional expense or service they’d need — they could just get going
\n
\nIt’s a great idea — no question. Given that my goal is to get as many people as possible using RSS, this makes total sense.
\nWhy we didn’t ship with this feature
\nFor the first release — I still think of it as a 1.0, because it really is — our best bet was to appeal to people already using an existing RSS service. We know that those people like and use RSS, and they’re the people most likely to check out a new RSS app.
\n(We could have delayed and shipped with support for more existing services, but we figured one was enough to get started with, and we could add other services later. And we are.)
\nIn other words, we tried to make an app that the existing market would like. And that’s the right call when you’re starting out.
\nAlso: iCloud sync makes the most sense when you have both a Mac and an iOS app, and we don’t — the iOS app is still in progress. We totally expect people to use NetNewsWire on the Mac and Unread or Reeder on their iPhone and iPad — and iCloud sync won’t work across apps. This scenario requires using services such as Feedbin.
\nWhy I have no idea when this feature might appear
\nFor any existing RSS service, we can be confident that our effort to support it in NetNewsWire would be successful. This is well-trodden ground: we make some web API calls, integrate with our database, and done. It’s not nothing, but conceptually it’s simple and there’s no cause to worry about technical issues.
\nBut iCloud syncing will mean writing exploratory code and only then finding out if it’s going to work.
\nSyncing the feeds list should be relatively easy — the real issue is with syncing read/unread/starred states of articles. That means a lot of small records.
\nIs CloudKit up to this? What are the limits? How fast is it? How reliable?
\nWe just don’t know.
\nYes, it’s encouraging that News Explorer has this feature — but that doesn’t tell us much about the limits, reliability, and performance.
\nWorking on this is a risk.
\nSo — as you can imagine — we’re still more keen on supporting existing RSS services, because we know there are plenty of people who for-sure like RSS, and who might like NetNewsWire, but who won’t switch their syncing system just to use NetNewsWire.
\nThat said: I do think we’ll get around to trying this, and I’ll be super-pleased if it works, because it really is a great idea — but we have a bunch of other work to do first. (Including the iOS app!)
"},"alternate":[{"href":"https://inessential.com/2019/09/06/on_syncing_netnewswire_using_icloud","type":"text/html"}],"crawled":1569829821629,"title":"On Syncing NetNewsWire Using iCloud","published":1567817061000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8","label":"Uncategorized"}]},{"originId":"https://inessential.com/2019/09/06/_markos_charatzas_writes_https_qnoid_com","fingerprint":"ca200f5a","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d992:18991ffa","summary":{"direction":"ltr","content":"Markos Charatzas writes about his excitement in joining the Apple developer world in 2009 to his eventual disillusionment today.
"},"alternate":[{"href":"https://inessential.com/2019/09/06/_markos_charatzas_writes_https_qnoid_com","type":"text/html"}],"crawled":1569829821629,"published":1567788970000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8","label":"Uncategorized"}]},{"originId":"https://inessential.com/2019/09/04/on_the_many_netnewswire_feature_requests","fingerprint":"66196df9","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d991:18991ffa","summary":{"direction":"ltr","content":"A number of people have asked that NetNewsWire show the full web page — right there, in the app — after clicking a link.
\nThe idea is pretty good! It solves two big problems:
\n\n- You get full content, which is great when a feed contains only summaries or truncated articles
\n- You don’t have to switch to another app: you can stay right where you are
\n
\nYou’d think it’s a no-brainer, and we should just go ahead. But there are other considerations.
\nOne big one is that your ad blockers and privacy extensions won’t run. They work in Safari, but they do not extend to other apps that use WebKit. This means that viewing a web page in NetNewsWire would be less secure and more annoying than viewing the same page in Safari (or whatever your browser is).
\nThis points to one of my design principles: the app should have boundaries. Some features belong in the app, and some features are best left to apps that do that feature way better than NetNewsWire could. One of those things is showing web pages — that’s really a web browser feature.
\nHaving boundaries means we can concentrate on doing a great job at the things that do belong in the app.
\n(Before you mention SFSafariViewController, recall that it’s iOS-only.)
\nWhat about the glory days?
\n“But Brent! In NetNewsWire 2.0 you added a tabbed browser to NetNewsWire, and it was awesome and a hugely popular feature!”
\nIt was! But times have changed. Many websites are hostile these days. In 2005, this feature was fine — but these days it’s totally not.
\nA winged messenger arrives with a solution
\nThere is a solution to the problem of showing full content and not leaving the app, and it’s a feature that really does belong in an RSS reader: using content extraction to grab the article from the original page.
\nIf you’ve ever used Safari’s Reader view, then you know what I’m talking about. The idea is that NetNewsWire would do something very much like the Reader view (but inline, in the article pane), that grabs the content and formats it nicely, without all the extra junk that is not the article you want to read.
\nThere are a number of open source options for this. We’re looking at using Feedbin’s content extraction service (which wouldn’t require you to have a Feedbin account).
\nThe generous folks at Feedbin are running a copy of the open-source Mercury Parser, and they’ve offered to open this service up to RSS readers like NetNewsWire. (Reeder uses it already, for instance.)
\nWhen?
\nRight now we’re working on NetNewsWire 5.0.1, which is (almost entirely) a bug-fix release. I don’t know what’s going to be in 5.1 yet — we’re still digesting all the feedback, looking at our original roadmap, and thinking about things.
\nWe’re also working on NetNewsWire for iOS! We’re busy.
\nBut this is definitely the kind of feature that should come sooner rather than later.
"},"alternate":[{"href":"https://inessential.com/2019/09/04/on_the_many_netnewswire_feature_requests","type":"text/html"}],"crawled":1569829821629,"title":"On the Many NetNewsWire Feature Requests to Show Full Web Pages","published":1567661107000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8","label":"Uncategorized"}]},{"originId":"https://inessential.com/2019/09/02/on_my_funny_ideas_about_what_beta_means","fingerprint":"bb260103","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d990:18991ffa","summary":{"direction":"ltr","content":"John Gruber has mentioned, on The Talk Show, that I’ve got some weird ideas about what beta means.
\nHere are my definitions:
\ndevelopment (d): everything is in progress and the app might be completely unusable.
\nalpha (a): the app is feature-complete and has no known bugs — but, importantly, it’s had very little testing.
\nbeta (b): the app is feature-complete, has no known bugs, and has been tested — but further testing is still warranted. Every beta is a release candidate.
\nThese are defined in a NetNewsWire Technote. It’s important to have definitions that everybody working on or testing the app understands.
\nBut why these rather strict definitions?
\nIt’s part of our commitment to quality. What matters is the end result — the shipping app — and these definitions make sure we don’t get to beta, or even alpha, with the app up on the table with wires sticking out and pieces missing.
\nThis gives us a big space between development and shipping, and that space is all about making sure the bugs are all fixed.
\nThis is a matter of ethics and pride in our work. Absolutely.
\nBut it’s also pragmatic. This is an open source app, written by volunteers in their spare time, and having this rhythm baked-in to the process helps make sure we can uphold our standards even without full-time developers, managers, and testers.
\n* * *
\nAnd… it bugs me how little real attention our industry pays to quality these days. In some cases the consequences are disastrous; in other cases they’re merely expensive. It doesn’t have to be this way.
\nIf it seems like I’m going too far with my definitions, well, I’m trying to bend the stick here.
"},"alternate":[{"href":"https://inessential.com/2019/09/02/on_my_funny_ideas_about_what_beta_means","type":"text/html"}],"crawled":1569829821629,"title":"On My Funny Ideas About What Beta Means","published":1567455823000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8","label":"Uncategorized"}]},{"originId":"https://inessential.com/2019/08/31/i_love_this_netnewswire_write_up_on_wp_t","fingerprint":"e526eb19","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d98f:18991ffa","summary":{"direction":"ltr","content":"I love this NetNewsWire write-up on WP Tavern.
"},"alternate":[{"href":"https://inessential.com/2019/08/31/i_love_this_netnewswire_write_up_on_wp_t","type":"text/html"}],"crawled":1569829821629,"published":1567280353000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8","label":"Uncategorized"}]},{"originId":"https://inessential.com/2019/08/31/netnewswire_5_feature_requests","fingerprint":"57ea983b","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d98e:18991ffa","summary":{"direction":"ltr","content":"NetNewsWire 5.0 is a 1.0 app in disguise.
\nAnd so, as expected, we’ve had a ton of feature requests. Most people tend to request one or two features — and there’s a huge variety in these. People want different things.
\nNevertheless, there are a few themes we can pick out from what people are asking for:
\n\n- More syncing options, especially Feedly support
\n- iOS app
\n- Some way to deal with partial-content feeds
\n- Customization of the article pane (fonts, colors, etc.)
\n- Traditional view (timeline on top with single lines, article below)
\n- More sharing options (Instapaper, Pinboard, etc.)
\n- Customizable keyboard shortcuts
\n- State restoration
\n- Localizations
\n- Hiding read items in the timeline (or dimming them)
\n- Hiding feeds (in the sidebar) that have no unread articles
\n- User-created smart feeds
\n
\nThe less-common, more singular requests are for things like specific sorting options — there are lots of different small options that people would like.
\nPeople have also asked for things that might surprise you (they surprised me) — for instance, we’ve had a request for monochrome icons for the toolbar. Another request for a Dark Mode that’s different from Apple’s Dark Mode. Etc.
\nHow We Choose What To Do Next
\nThe first principle is that we can’t lose what we love about the app. We do our damnedest to ship with no bugs, and the app needs to be fast and, most importantly, it needs to feel lighter-than-air.
\nWhenever you add things — even if the app remains just as fast, even if there are no bugs — you still run the risk of losing that feeling of lightness. One of the quickest ways to lose that feeling is to add a whole bunch of preferences, View menu options, toolbar commands, and other chrome. So we’re going to be very slow to add things like that.
\nNetNewsWire needs to not become fiddly. (Earlier versions of NetNewsWire got way too fiddly.)
\nThere are other questions we ask about a feature before we do it.
\n\n- Will it substantially benefit current users?
\n- Will it bring a number of new users to the app?
\n- Does the feature depend on something else being done first?
\n- How much work will it take?
\n- Does it require resources (such as new icons) that our programmers can’t provide?
\n- Does the feature really belong in an RSS reader at all?
\n
\nAnd, because this is an open source app, there’s another dimension: people. Is someone available? Has someone just shown up who’s eager to work on a specific feature? Those things have an impact on scheduling, too.
\nThe good news is that most of the common feature requests are obvious things to do.
\nSome examples — not nearly everything, just a few thoughts:
\nThe iOS app is in progress. Maurice Parker has been writing it, and it’s coming along very well. Still plenty more to do, and we won’t ship before iOS 13 ships, but it’s happening.
\nAdding syncing options is a definite good thing for the app. Doing the first one (Feedbin) was the big effort, because it required building the infrastructure that makes syncing possible. Once that was done, adding additional services is not super-difficult. (Not easy, no. Nothing’s trivial. But at least the infrastructure and patterns are in place.)
\nWe’d like to support all the various services, or at least a majority of them. And we have people working on adding services.
\nCustomization of the article pane will most likely work the way it did in older versions of NetNewsWire: we had theme files which included templates and CSS. The app shipped with a few, and you could make your own and use themes other people made.
\nThis feature shipped with NetNewsWire 2.0, and people really loved it. It was fun!
\nMore sharing options is an obvious good idea. Of course you should be able to send to Instapaper, Pocket, Pinboard, and so on. We shipped with custom support for MarsEdit and the Micro.blog app — mainly because I use those apps. But an RSS reader ought to support as many sharing workflows as possible. That’s one of the core points of the app.
\n* * *
\nAnyway — the above doesn’t cover everything. Don’t take any of the above as gospel about what we’re doing or when, or what we’re not doing. We haven’t planned 5.1 yet! It’s too soon.
\nThere are also features that we want to do that people haven’t asked for, but that we think are cool. \uD83C\uDFB8
\nThe take-away from this article should be: we’re being very careful about designing and implementing new features, because we have to make sure NetNewsWire doesn’t lose what makes it special.
\nBut we are doing new features, because there are so many things that can make the app even better — we can make it better for current users and we can bring in new users.
"},"alternate":[{"href":"https://inessential.com/2019/08/31/netnewswire_5_feature_requests","type":"text/html"}],"crawled":1569829821629,"title":"NetNewsWire 5 Feature Requests","published":1567278518000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8","label":"Uncategorized"}]},{"originId":"https://inessential.com/2019/08/29/follow_through","fingerprint":"444937b","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d98d:18991ffa","summary":{"direction":"ltr","content":"Decades ago, when I was working for Dave Winer at UserLand, I learned about the concept of follow-through after a major release.
\nIf you’re an app maker, it might seem like your goal is to get to release day. Get the app done, make it available, publish an announcement, and then get back to coding. Let the world do what it’s going to do.
\nOne bang, and then back to work, in other words.
\nBut that’s not going to maximize your chances for a good release. You need to follow through — you need to keep going.
\nSome of the things you might do, in no particular order:
\n\n- Publish tips on using your app — one a day or so
\n- Update your website with feedback, testimonials, and good reviews
\n- Be available and communicative about your app
\n- Go on some podcasts
\n- Write about how release day went
\n- Write about plans for the x.0.1 version
\n- Field bug reports and feature requests gratefully
\n- Thank reviewers who’ve done a good job
\n- Make it as easy as possible for reporters and reviewers to get access to your app and to you
\n- Work to build a community of customers, on Slack or similar
\n
\nI’m sure you can think of more things to do — the above isn’t everything, and every app is different.
\nBut the key is that you don’t just do the release and then stop. Instead, show that you‘re responsive, show that your app has momentum, show that you care enough to keep showing up.
\nFor me, at least, this is the fun part. I realize that’s not true for everybody — but you should do it anyway. \uD83C\uDFA9
"},"alternate":[{"href":"https://inessential.com/2019/08/29/follow_through","type":"text/html"}],"crawled":1569829821629,"title":"Follow-Through","published":1567110304000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8","label":"Uncategorized"}]},{"originId":"https://inessential.com/2019/08/28/daniel_figures_out_one_of_the_two_crashi","fingerprint":"669e65c4","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d98c:18991ffa","summary":{"direction":"ltr","content":"We have a few reports of a crash where the add-feed-sheet window doesn’t load. There’s a line of code with window! — because of course we expect the window to have been loaded — and it crashes right there.
\nThis crash made zero sense to me, but Daniel Jalkut figured out the most likely cause and was able to reproduce it: it’s because the person has moved the app (from one folder to another) after launching it, while it’s running, and the nib-loading machinery can’t find the nib, because it’s moved along with the app.
\nTip: if you’re going to move an app, quit it first, then move it, and then re-launch it!
\nAt any rate: our fix for this will be to load that sheet on startup, and then recycle it on each use. This fix will go into NetNewsWire 5.0.1.
\nThis just fixes the bug with this one nib, though. A more systematic fix — maybe just a warning to the user suggesting they quit and re-launch — would be a good idea.
\nFile under “bugs iOS developers never have to worry about.” \uD83D\uDC07
\nPS We have a 5.0.1 beta milestone now.
"},"alternate":[{"href":"https://inessential.com/2019/08/28/daniel_figures_out_one_of_the_two_crashi","type":"text/html"}],"crawled":1569829821629,"title":"Daniel Figures Out One of the Two Crashing Bugs","published":1567022746000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8","label":"Uncategorized"}]},{"originId":"https://inessential.com/2019/08/27/how_release_day_went","fingerprint":"2394a816","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d98b:18991ffa","summary":{"direction":"ltr","content":"Yesterday was a great day! A few things to note, in no particular order:
\nNetNewsWire got some press coverage, including a well-done review in MacStories.
\nWe got a lot of feature requests, but no bug reports.
\nExcept that we did get a single-digit number of crash logs. On investigation, I found two distinct backtraces — we’ll need to fix those. The thing is, there’s no freakin’ way the app should crash in those spots. Except that, obviously, it can. Rarely, but it happens.
\nThe servers started timing-out at one point during the day. I contacted DreamHost support and they fixed things (and told me that the fixes they applied should prevent this in the future).
\nThere were a number of nice blog posts and tweets about NetNewsWire, which was awesome. After working so hard for so long, it’s great when people appreciate the app. We don’t get paid in money, after all. \uD83D\uDC23
\nI have no idea how many downloads of the app there were. GitHub is hosting the download, via its releases feature, and I don’t see a way to find out how many times it’s been downloaded. Which is totally fine with me.
\n* * *
\nI should say something more about the no-bug-reports. There’s no special magic or talent or anything to this — there’s just the willingness to say that we’re not going to ship until we’ve got the bugs out, and then sticking to that.
\nThis is a matter of pride and ethics, for sure, but there’s another dimension: since the app is open source, it’s written by volunteers (including me), and we have no dedicated support team. Any time we spend fielding bug reports is time taken away from working on the next feature.
\nMaking apps — even, or especially, free apps — is an exercise in economics. With free apps, the economics are even more constrained, because nobody is going to hire even a part-time support person. So we do everything we can do keep costs down — especially time costs.
\nPlus — buggy apps can be demoralizing to the people who work on them. Part of my job is to make sure people are proud and happy to work on the app. And that means making sure everyone knows we’re super-serious about doing our best to never ship bugs.
"},"alternate":[{"href":"https://inessential.com/2019/08/27/how_release_day_went","type":"text/html"}],"crawled":1569829821629,"title":"How Release Day Went","published":1566937707000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8","label":"Uncategorized"}]},{"originId":"https://inessential.com/2019/08/26/netnewswire_5_0_now_available","fingerprint":"175d2cdb","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d98a:18991ffa","summary":{"direction":"ltr","content":"
\nNetNewsWire 5.0 is shipping!
\nIn case you haven’t been following along until just now: NetNewsWire is an open source RSS reader for Mac. It’s free! You can just download it and use it. No strings.
\nIt’s designed to be stable, fast, and free of bugs. It doesn’t have a lot of features yet, and that’s because we prioritized quality over features. We will be adding more features, of course, but not quickly. We’re also working on an iOS app.
\nIt syncs using Feedbin. We’ll support more systems in the future (as many as possible).
\nI hope you like it!
\nSome links…
\n\nThanks to so many people
\nI want to especially thank Sheila Simmons and my family and friends.
\nThis release took five years to make, and for four of those years it wasn’t even called NetNewsWire. It was just a year ago that I got the name NetNewsWire back from Black Pixel — and I thank them again for their wonderful generosity.
\nI also want to thank Brad Ellis for making the beautiful app icon and toolbar icons. Thanks to our major code contributors: Maurice Parker, Olof Hellman, and Daniel Jalkut. Thanks to Ryan Dotson for writing the Help book. Thanks to Joe Heck for looking after infrastructure issues (especially continuous integration).
\nThanks to my co-workers and friends at The Omni Group (which is a wonderful place to work). Thanks to the ever-patient and ever-awesome NetNewsWire beta testers on the Slack group and elsewhere.
\nAnd thanks to everyone who’s ever used the app in its 17-years-and-counting run. Because of you, NetNewsWire has been, and remains, the thrill of my career.
"},"alternate":[{"href":"https://inessential.com/2019/08/26/netnewswire_5_0_now_available","type":"text/html"}],"crawled":1569829821629,"title":"NetNewsWire 5.0 Now Available","published":1566834451000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8","label":"Uncategorized"}]},{"originId":"https://inessential.com/2019/08/22/end_of_the_line_for_netnewswire_3_3_2","fingerprint":"e30daaa8","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d989:18991ffa","summary":{"direction":"ltr","content":"This is a little bit of bad news. It’s not my intention, and it’s not what I want to happen — but NetNewsWire 3.3.2 apparently does not launch in the next version of macOS (10.15, Catalina).
\nIt links to the PubSub framework, which is not included with the next macOS.
\nNetNewsWire 3.3.2 was the last release of the full version that I worked on, before selling NetNewsWire to Black Pixel, and I’ve heard from lots of people that they’ve been using it ever since. They never switched.
\nI would rather it continued working forever, but that’s not to be. Not my choice. Sorry about that!
"},"alternate":[{"href":"https://inessential.com/2019/08/22/end_of_the_line_for_netnewswire_3_3_2","type":"text/html"}],"crawled":1569829821629,"title":"End of the Line for NetNewsWire 3.3.2","published":1566515704000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8","label":"Uncategorized"}]},{"originId":"https://inessential.com/2019/08/21/the_netnewswire_blog_has_the_details_on_","fingerprint":"c583e740","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d988:18991ffa","summary":{"direction":"ltr","content":"The NetNewsWire blog has the details on NetNewsWire 5.0b5 — which should be the last beta.
\nStill planning to do the 5.0 final release Monday morning, which really means doing the release on Sunday and pushing an announcement to this blog Monday morning. :)
\nThe last things on my to-do list are actually writing that announcement and doing screenshots for the NetNewsWire web page. Easy. \uD83D\uDC2F
"},"alternate":[{"href":"https://inessential.com/2019/08/21/the_netnewswire_blog_has_the_details_on_","type":"text/html"}],"crawled":1569829821629,"published":1566452581000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8","label":"Uncategorized"}]},{"originId":"https://inessential.com/2019/08/20/immunization","fingerprint":"39a4bdb0","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d987:18991ffa","summary":{"direction":"ltr","content":"Before every major release I like to try and think of everything mean that people might say about the app. It’s fun!
\nSo we just went through this exercise on the NetNewsWire Slack group. Here’s a taste:
\n\n- This took five years? I could write an RSS parser in a weekend.
\n- Can’t get my Twitter and Facebook feeds. Whatever.
\n- Doesn’t work with my Usenet host.
\n- The information density of the timeline is… lacking. What the hell.
\n- Not truly open source since it’s on a Mac.
\n- Not truly open source since it’s not GPL.
\n- No vim keys. Why bother.
\n- Regular people will never use an RSS reader. What’s the point?
\n- Brent’s last good idea was in 2002. Consider this a textbook case of coasting.
\n- Great app. Too bad RSS died with Google Reader.
\n- It totally didn’t pick up my subscriptions from the earlier version. How is this an upgrade?
\n- When does a 5.0 have fewer features than a 3.0? When it’s NetNewsWire.
\n- The echo chamber will love this app. They always do.
\n- Free app. Continues the race to the bottom. Pour one out for Silvio Rizzi.
\n- No way to send to Instapaper. Fuck it.
\n- Brent Simmons can’t stop pursuing a technology that even Google famously admitted was not worth bothering with.
\n- If this app took five years, imagine how long it will take before it will actually sync with Feedly.
\n- Sure it’s free, but I bet the Feedbin people paid them off, because the only way to sync is to pay money to Feedbin.
\n- No iCloud sync? Jerks.
\n- No iOS app. The revolution happened on mobile, Brant. What the actual fuck.
\n- Shoulda been Catalyst. Dinosaurs wrote this app.
\n- Not on the Mac App Store? I guess they don’t want users.
\n- I would totally use this if it had just this one [feature x], which I can’t believe they shipped without. (Multiply this comment by 100, with a different feature x each time.)
\n- Area Man Can’t Let RSS Go
\n
\nSome feedback will be factually inaccurate, but we like to imagine that too:
\n\n- I remember using NetNewsWire on OS 9, and it hasn’t really improved since then. They should make it a Cocoa app.
\n- Doesn’t work with web comics. POS
\n- Doesn’t support 10.5.
\n- It should be free.
\n- You’d think they would have updated the design — but it looks exactly like NetNewsWire of old.
\n- Why the hell would they build on that aging code base from Black Pixel? I heard it doesn’t even use ARC.
\n- No way to sync? What’s their actual problem?
\n
\nSee? The actual feedback will be nicer than the stuff we thought up. This provides a bit of immunization. :)
\nBut, also, there will be negative feedback we didn’t imagine. That’s the gold!
\n* * *
\nBonus from Daniel Jalkut, but not actually a criticism:
\n\nCan’t innovate, my RSS.
\n
"},"alternate":[{"href":"https://inessential.com/2019/08/20/immunization","type":"text/html"}],"crawled":1569829821629,"title":"Immunization","published":1566332363000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8","label":"Uncategorized"}]},{"originId":"https://inessential.com/2019/08/19/i_think_were_still_on_track_for_releasin","fingerprint":"592043f","id":"ITR2bp1hhxjNSFKlSuZR7gUTTcxmHRq2TwhCgV9CifI=_16d81261cbd:4d986:18991ffa","summary":{"direction":"ltr","content":"I think we’re still on track for releasing NetNewsWire 5.0 Monday, August 26. There will be one more beta before then.
\nI’ll be available for podcasts, interviews-via-email, etc. If you’d like to set something up, email me or DM me on Twitter.
"},"alternate":[{"href":"https://inessential.com/2019/08/19/i_think_were_still_on_track_for_releasin","type":"text/html"}],"crawled":1569829821629,"published":1566259329000,"origin":{"streamId":"feed/http://inessential.com/xml/rss.xml","htmlUrl":"https://inessential.com/","title":"inessential.com"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8","label":"Uncategorized"}]},{"id":"zl8fQ5xTLhjt3vC6/I1d1cHoXwVcArEYV8/6kICnZkA=_16d8125cccf:1a5d:90d684ff","originId":"http://www.mechanicalgirl.com/post/using-google-cloud-functions-create-simple-post-endpoint-handle-data/","fingerprint":"6c878b73","title":"Using Google Cloud Functions to Create a Simple POST Endpoint","summary":{"content":"\nI was tempted to title this "how to use GCF to create a simple ETL process", but that's not quite what I'm demonstrating here.\n
\n\nIt does loosely fit the description of an ETL process - the script extracts some values from a POSTed payload, rearranges some of the values to fit a specific schema, then loads the transformed payload into a data store.
\n\nBut what you're going to see here is not the heavy lifting we normally think of when we see the acronym "ETL".\n
\n\nAnd maybe that's a good thing, as it illustrates the beautiful simplicity of Google's new Cloud Function service.\n
\nSome background:
\n\nI work on a data infrastructure team that already has an account and a project set up on Google Cloud Platform. That project is already associated with a data store - a BigQuery project/dataset. I'm not going to cover how to set all that up since it's out of scope here, but you can start with these docs: https://cloud.google.com/docs/\n
\n\nI'm currently working on a project to accept realtime event data from a media platform we work with. We expect the data to come in at a medium-to-high volume, but we're still in testing so I don't have details on how well this job will handle the volume or how well it will scale - that will come later.\n
\nThe project:
\n\nWhat I am going to talk about is this flow, with some general info on how to build the tools I needed to handle each step:\n
\n\n- vendor POSTs the event data payload to my http endpoint
\n- we receive, validate, and transform the payload data
\n- we write that data to a BigQuery table
\n
\n\nThe pieces I had to build to do this:\n
\n\n- a Google Cloud Function
\n- a BigQuery table
\n
\ngcloud:
\n\nBefore we go much further - assuming that you already have a Google Cloud project, with a BigQuery dataset, and all the permissions set up to link the two - you will also need the gcloudcommand line tool. Go here and follow the steps to install:\n
\n\nhttps://cloud.google.com/sdk/docs/quickstart-macos\n
\n\ngcloud is what you'll use to deploy your function to Google Cloud. Installation will update your PATH to include the Google Cloud SDK in ~/.bash_profile. You may need to go through some authorization steps using the email address you have associated with your project. You may not need to add any gcloud components, although if you do instructions are included in the installation output.\n
\n\nFor the example here, you should probably have these components:\n
\n\n- BigQuery Command Line Tool
\n- Cloud SDK Core Libraries
\n- Cloud Storage Command Line Tool
\n
\nSetting up the script:
\n\nIn a local folder, do some of the basic setup you normally would to start a Python project:\n
\n\n- create a
main.py - this wil be your script \n- add a
requirements.txt for any libraries you might need to install \n- use
virtualenv to keep everything contained, particularly if you're going to test locally \n
\n\nIn your main.py, you are free to build your Python script in whatever way works for you. You can import any libraries you might need, you script structure can be as simple or as complex as you need it to be.\n
\n\nThe only key requirement is that you name a function that will be the entry point for your script - that name will be how your function is referenced in the GCP dashboard, and will be used to deploy the code to GCP.\n
\nThe code:
\n\nNow (finally!) let's look at some sample code:\n
\n\nIn main.py, I've built a simple Flask app (I love how easily Flask handles POSTs).\n
\n\nimport json\n\nfrom flask import Flask, request\nfrom google.cloud import bigquery\n\napp = Flask(__name__)\n
\n\nI'll use this schema both to create my BigQuery table and to insert rows. This schema example includes several common column types used in BigQuery.\n
\n\nschema = [\n bigquery.SchemaField('timestamp', 'TIMESTAMP', 'NULLABLE'),\n bigquery.SchemaField('event_type', 'STRING', 'NULLABLE'),\n bigquery.SchemaField('event_id', 'STRING', 'NULLABLE'),\n bigquery.SchemaField('has_insights', 'BOOLEAN', 'NULLABLE'),\n bigquery.SchemaField('video_insights', 'RECORD', 'REPEATED',\n fields=[\n bigquery.SchemaField('video_id', "STRING", 'NULLABLE'),\n bigquery.SchemaField('video_duration', "INTEGER", 'NULLABLE'),\n ],\n ),\n bigquery.SchemaField('categories', 'STRING', 'REPEATED'),\n]\ndataset = 'my_dataset'\ntable_name = 'my_events_table'\n
\n\nHere's the events() function that does a few things:\n
\n\n- simple validation of the POST contents, the 'payload' (make sure it's JSON and that 'events' are included)
\n
\n\ndef events(request):\n payload = {}\n try:\n payload = request.get_json()\n events = payload['events']\n except Exception as e:\n response = app.response_class(\n response=json.dumps({'error': e.message}),\n status=400,\n mimetype='application/json'\n )\n return response\n
\n\n- create the table in BigQuery (or not - this can be done in a few different ways, I've just included the example here as a convenience)
\n
\n\n try:\n create_table()\n except Exception as e:\n print("ERROR", e)\n
\n\n- for each event, extract a few values, add them to a dict, add that dict to a new list (this is the 'transform' part of our mini-ETL)
\n
\n\n event_rows = []\n for p in payload['events']:\n entry = construct_entry(payload, p)\n event_rows.append(entry)\n
\n\n- take that list of transformed events and insert them into the BigQuery table all at once
\n
\n\n try:\n insert_entries(event_rows)\n except Exception as e:\n print("Error on inserting entries: %s" % e)\n sys.exit()\n
\n\n- and if it's all successful, return a 200
\n
\n\n response = app.response_class(response='', status=200)\n return response\n
\n\nHere are the utility functions that use the google.cloud.bigquery library to do all that stuff:\n
\n\n- create a BigQuery table
\n- transform each event into an entry
\n- load the list of entries into the BigQuery table
\n
\n\ndef create_table():\n client = bigquery.Client()\n dataset_ref = client.dataset(dataset)\n table_ref = dataset_ref.table(table_name)\n table = bigquery.Table(table_ref, schema=schema)\n table = client.create_table(table)\n print("Created table {}".format(table.full_table_id))\n return table.full_table_id\n\ndef construct_entry(payload, event):\n insights_list = []\n if event.get('video_insights', None):\n for i in event['video_insights']:\n v = {\n 'id': vp.get('video_id', ''),\n 'time_played': vp.get('video_duration', 0),\n }\n insights_list.append(v)\n entry = {\n 'timestamp': event.get('timestamp', None),\n 'type': event.get('event_type', ''),\n 'id': event.get('event_id', ''),\n 'categories': event.get('categories', []),\n 'has_insights': event.get('has_insights', False),\n 'insights': insights_list,\n }\n return entry\n\ndef insert_entries(event_rows):\n client = bigquery.Client()\n dataset_ref = client.dataset(dataset)\n table_ref = dataset_ref.table(table_name)\n table = bigquery.Table(table_ref, schema=schema)\n\n try:\n response = client.insert_rows(table, event_rows)\n except Exception as e:\n print("Error: %s" % str(e))\n return False\n return True\n
\n\ngoogle.cloud.bigquery docs are here: https://googleapis.github.io/google-cloud-python/latest/bigquery/reference.html\n
\n\nFinally here's the main method that uses Flask to run the app and route requests to the events() function:\n
\n\nif __name__ == '__main__':\n app = Flask(__name__)\n app.route('/events', methods=['POST'])(lambda: events(request))\n app.run(debug=True)\n
\n\nAltogether, that's only 100 lines of code! My example does work with a simplified payload, so as always, your mileage may vary.\n
\n\nI only have three libraries in my requirements.txt:\n
\n\nFlask==1.0.2\ngoogle-cloud-bigquery==1.3.0\ngoogle-cloud-storage==1.6.0\n
\nThe sample payload:
\n\nAnd here's what you would expect a valid payload to look like for this example:\n
\n\n{\n "site_id": "example.com",\n "another_id": "FE7169C2",\n "events": [\n {\n "timestamp": "2017-07-25T09:15:36Z",\n "event_type": "ARTICLE_VIEW_EVENT",\n "event_id": "28C86A6C-B93F-4445-94D0-5926F6C0F723",\n "categories": ['Technology', 'Computers', 'News'],\n "has_insights": false\n }, {\n "timestamp": "2017-07-25T10:03:12Z",\n "event_type": "ARTICLE_VIEW_EVENT",\n "event_id": "A7ED75A5-475E-44EE-BAD9-3A57D8F547B2",\n "categories": ['Entertainment', 'Games'],\n "has_insights": true\n "video_insights": [\n {\n "video_id": "video1",\n "video_duration": 120\n }\n ]\n }\n ]\n}\n
\nDeploying:
\n\nTo deploy from the command line, make sure you're in the folder with the function code. Then run:\n
\n\ngcloud beta functions deploy events --trigger-http --runtime python37 --project my-project-name\n
\n\nBreaking it down:\n
\n\ngcloud beta functions deploy is the command that lets you create or update a Google Cloud Function \nevents is the name of the Google Cloud Function (as defined in example source code) that will be executed - you'll also see this in the dashboard \n- we specify
--trigger-http to generate an http endpoint where our function can receive requests \nruntime is the execution environment (there are also node.js and golang options) \n- finally, you need to include your project name
\n
\n\nFor more info about constructing a deployment, run:\n
\n\ngcloud beta functions deploy --help\n
\n\nWhen your deploy is successful, you'll see the entry point/http trigger values included in the return message, looking something like this:\n
\n\nentryPoint: events\nhttpsTrigger:\n url: https://region-my-project-name.cloudfunctions.net/events\n
\n\nAnd that's it! If you use CURL to post a valid payload to your new endpoint, you should shortly thereafter see a few records in your BigQuery table.\n
\nEtcetera:
\n\nI would guess that HTTP endpoints are going to be the most common use for Google Cloud Functions, but there are several other trigger types available. For more, take a look at:\n
\n\nhttps://cloud.google.com/functions/docs/calling/\n
\n\nYou should put your code in Github or whatever your choice of repository is, but be aware that GCP also stores the most recent version of the source code. In your project, navigate to the functions dashboard, e.g.:\n
\n\nhttps://console.cloud.google.com/functions/list?project=my-project-name\n
\n\nAnd click through to your-function-name >> Source.\n
\n\nIf you poke around your function dashboard on the GCP console, you'll also find some fun stuff like usage and activity charts, logging, and a little inline testing module.\n
\n\nFinally, here's a really good primer on GCF. This post is what that got me started:\n
\n\nServerless Python Quickstart with Google Cloud Functions (Dustin Ingram)\n
","direction":"ltr"},"alternate":[{"href":"http://www.mechanicalgirl.com/post/using-google-cloud-functions-create-simple-post-endpoint-handle-data/","type":"text/html"}],"crawled":1569829801167,"published":1569829801167,"origin":{"streamId":"feed/http://www.mechanicalgirl.com/feeds/all/","title":"MechanicalGirl","htmlUrl":"http://www.MechanicalGirl.com/"},"visual":{"url":"http://www.fubiz.net/wp-content/uploads/2013/10/Modern-Eco-Friendly-Home5-493x1024.jpg","width":493,"height":1024,"contentType":"image/jpeg"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8","label":"Uncategorized"}]}]}
\ No newline at end of file
diff --git a/Frameworks/Account/AccountTests/Feedly/Initial/weblogs_initial.json b/Frameworks/Account/AccountTests/Feedly/Initial/weblogs_initial.json
deleted file mode 100644
index df70e510d..000000000
--- a/Frameworks/Account/AccountTests/Feedly/Initial/weblogs_initial.json
+++ /dev/null
@@ -1 +0,0 @@
-{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/e31b3fcb-27f6-4f3e-b96c-53902586e366","updated":1569830765949,"continuation":"16d74074eb8:2058:90d684ff","items":[{"originId":"https://useyourloaf.com/blog/scroll-view-layouts-with-interface-builder/","fingerprint":"3edfc038","id":"AVw5dsyytPMMU/iCsYRiYJstfLF5CXoDiC5tFHJ2Gfk=_16d8134857d:4da39:18991ffa","author":"keith@useyourloaf.com (Keith Harrison)","summary":{"direction":"ltr","content":"Laying out a scroll view is a confusing task. I found it easier when, in iOS 11, Apple introduced frame and content layout guides. Too bad they neglected to add them to Interface Builder. That changed in Xcode 11. Here’s a quick guide on how to use them.
\nWhy Are Scroll View Layouts So Hard?
\nFor a recap on why scroll views are confusing and how the frame and content layout guides help see this earlier post on easier scrolling with layout guides. Here’s the layout I built in code using layout guides:
\n
\nYou need two groups of constraints to layout a scroll view:
\n\n- constraints that fix the frame (size and position) of the scroll view relative to its superview.
\n- constraints that layout the content relative to the content area of the scroll view and constrain its size.
\n
\nBuilding my layout with Interface Builder and Xcode 10 I might end up with constraints like this:
\n
\n\n- Four constraints pin the scroll view to the edges of the root view, fixing its frame.
\n- Four constraints between the stack view and the scroll view pin the content to the scroll view content area.
\n- The width of the stack view is fixed to the width of the scroll view frame.
\n
\nI never found this to be very intuitive:
\n\n- Constraints between the scroll view and a super view act on the frame of the scroll view.
\n- Constraints between the scroll view and a subview act on the content area of the scroll view.
\n- Height or width constraints between the scroll view and a subview use the frame of the scroll view.
\n
\nThe introduction of the frame and content layout guides in iOS 11 promised to make things clearer. Unfortunately, Apple neglected to add support for them in Interface Builder.
\nWhat Changed In Xcode 11
\nFrom the Xcode 11 release notes:
\n\nContent and Frame Layout guides are supported for UIScrollView and can be enabled in the Size inspector for more control over your scrollable content. (29711618)
\n
\nThe content and frame layout guides are enabled by default in Xcode 11 when you drag a new scroll view into the canvas:
\n
\nYou can enable them for an older layout using the size inspector:
\n
\nThe best way to explain how to use them is with an example.
\nUsing The New Layout Guides In Interface Builder
\nLet’s build the previous layout in Interface Builder using the content layout guides:
\n\nTo get started I have arranged the content for my layout in a vertical stack view that I have embedded in a scroll view in Interface Builder:
\n
\nLet’s fix the size and position of the scroll view by pinning it to the edges of the root view. I find that easier to do if you first hide the safe area layout guide for the root view (using the size inspector):
\n
\nSelect the scroll view and then use the “Add New Constraints” tool to add the four constraints between the scroll view and the edges of the root view:
\n
\nCheck the constraints in the navigator to make sure you’re not using the safe area or accidentally adding padding to any of the constraints:
\n
\nWe want to pin the stack view to the edges of the content area. Control-drag from the stack view to the Content Layout Guide:
\n
\nThen add the four constraints to pin each edge of the stack view to the guide:
\n
\nCheck the constraints in the navigator:
\n
\nI don’t want the layout to scroll horizontally so I also need to fix the width of the content area to match the width of the scroll view frame. It’s not possible to create an equal width constraint between the content and frame layout guides in Interface Builder. Instead, control-drag from the stack view to the frame layout guide:
\n
\nThen add an equal widths constraint:
\n
\nThe final set of nine constraints using both the content and frame layout guides:
\n
\n
\nWhat’s Still Missing?
\nI don’t want to complain. It’s good to see the content layout guides show up in Interface Builder. It’s been two years since they were introduced with iOS 11 but better late than never. To complete the picture I’d like to see Apple also expose the layout margin guide for the scroll view.
\nSometimes you want a view that remains fixed in the scroll view frame so that it floats over the scrollable content. For example, the info button in my previous example remains fixed in the top-left corner when the content scrolls:
\n
\nThe frame layout guide doesn’t help in this case as it extends into the safe area behind the navigation bar. I created constraints in code to fix the button to the layout margins guide of the scroll view:
\ninfoButton.leadingAnchor.constraint(equalTo:\n scrollView.layoutMarginsGuide.leadingAnchor),\ninfoButton.topAnchor.constraint(equalTo:\n scrollView.layoutMarginsGuide.topAnchor)\n
\nIf you try to do that in Interface Builder the constraints are assumed to be with the content area so the button scrolls with the content. The closest you can get is to use the safe area layout guide of the scroll view (enable it in the size inspector) with some extra padding:
\n
\nSee The Code
\nYou can find the code for this post in my Code Examples GitHub repository. You can compare it to the original ScrollGuide project which builds the layout in code.
\nRead More
\n
\n\nScroll View Layouts With Interface Builder was originally posted 16 Sep 2019 on useyourloaf.com.\n
\nWant this direct to your inbox? Sign up and get my free iOS Size Classes guide.\n
\n
"},"alternate":[{"href":"https://useyourloaf.com/blog/scroll-view-layouts-with-interface-builder/","type":"text/html"}],"crawled":1569830765949,"title":"Scroll View Layouts With Interface Builder","published":1568624542000,"origin":{"streamId":"feed/http://useyourloaf.com/blog/rss.xml","htmlUrl":"https://useyourloaf.com/blog/","title":"Use Your Loaf - iOS Development News & Tips"},"visual":{"url":"http://b.vimeocdn.com/ts/442/609/442609877_1280.jpg","width":1280,"height":720,"contentType":"image/jpeg"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/e31b3fcb-27f6-4f3e-b96c-53902586e366","label":"Weblogs"}]},{"originId":"https://useyourloaf.com/blog/editing-a-swift-package/","fingerprint":"1299da46","id":"AVw5dsyytPMMU/iCsYRiYJstfLF5CXoDiC5tFHJ2Gfk=_16d8134857d:4da38:18991ffa","author":"keith@useyourloaf.com (Keith Harrison)","summary":{"direction":"ltr","content":"When I want to make changes to a Swift package, I find it useful to do it in the context of a project that depends on that package. Unfortunately, you can’t directly edit a Swift package you add as a dependency to an Xcode project. It’s read-only and managed by Xcode. What you can do is add a local copy of the package that overrides the package dependency.
\nSwift Package Dependencies
\nI previously covered creating Swift packages in Xcode 11 and then adding them as a dependency to a project. Xcode downloads the package files from the remote repository:
\n
\nYou can browse the source files in the package, but you cannot edit them in Xcode (which is probably for the best).
\nEditing A Standalone Package
\nYou always have the option to clone the package repository to a new folder:
\n$ git clone <URL of Git repository>\n
\n
\nYou can then open the Package.swift file in Xcode and work on the package directly:
\n
\nAfter you’ve made your changes, commit the package back to the remote repository.
\nThis is cumbersome if you want to try your updated package with an existing Xcode project. To get Xcode to download the updates from the remote repository you either need to tag the commit with a new version or change the project dependencies to refer to the specific commit:
\n
\nLuckily, there’s a better way.
\nEditing A Local Copy
\n\nClone the package repository to a local directory as before.
\nDrag the whole folder into the Xcode project that has a dependency on the package:
\nXcode overrides the package dependency with the same name as the local package. Notice in the screenshot how the Swift Package dependency disappears from the file navigator.
\n
\nMake your changes to the local package and test them with the project before pushing the changes to the remote repository.
\nRelease the changes by tagging the commit and then pushing the tag:
\n
\n
\nTo switch back to using the remote package:
\n\nDelete the local copy of the package from the project.
\nMake Xcode update the package dependencies File > Swift Packages > Update To Latest Package Versions.
\nYou should now see the latest version of the remote package in the file navigator:
\n
\n
\n\nEditing A Swift Package was originally posted 19 Aug 2019 on useyourloaf.com.\n
\nWant this direct to your inbox? Sign up and get my free iOS Size Classes guide.\n
\n
"},"alternate":[{"href":"https://useyourloaf.com/blog/editing-a-swift-package/","type":"text/html"}],"crawled":1569830765949,"title":"Editing A Swift Package","published":1566205065000,"origin":{"streamId":"feed/http://useyourloaf.com/blog/rss.xml","htmlUrl":"https://useyourloaf.com/blog/","title":"Use Your Loaf - iOS Development News & Tips"},"visual":{"url":"http://b.vimeocdn.com/ts/442/609/442609877_1280.jpg","width":1280,"height":720,"contentType":"image/jpeg"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/e31b3fcb-27f6-4f3e-b96c-53902586e366","label":"Weblogs"}]},{"keywords":["Tech Life"],"originId":"https://randsinrepose.com/?p=4326","fingerprint":"84f0fac2","id":"xoPmejgsoSEqDaFR4LV9EjzHAN8HPWA8MceMFxcQR3k=_16d812a78cb:4da16:18991ffa","author":"rands","summary":{"direction":"ltr","content":"(Ok, first hit play on the above video. This article has a soundtrack.) In World of Warcraft, Darkshore is the first major city outside of the Night Elf starting area and stands in dark contrast to the mystical and deeply saturated pink realm of Teldrassil. It’s where you discover that the game is full of… More"},"alternate":[{"href":"https://randsinrepose.com/archives/ok-im-in-darkshore-and-it-just-started-raining/","type":"text/html"}],"crawled":1569830107339,"title":"Ok, I’m in Darkshore, and It Just Started Raining","published":1569074623000,"origin":{"streamId":"feed/http://www.randsinrepose.com/index.xml","htmlUrl":"https://randsinrepose.com","title":"Rands in Repose"},"content":{"direction":"ltr","content":"\n(Ok, first hit play on the above video. This article has a soundtrack.)
\nIn World of Warcraft, Darkshore is the first major city outside of the Night Elf starting area and stands in dark contrast to the mystical and deeply saturated pink realm of Teldrassil. It’s where you discover that the game is full of many people of many races. I’ve spent just over twenty hours in the game over a week, I’m crossing level 17, and it just started raining.
\nAs part of their fifteenth anniversary, Blizzard released the original “vanilla” version of World of Warcraft (“WoW”). This release represents the initial version of the wildly popularly MMO that consumed years of my life. There is a service on the internet that will tell me precisely the number of hours, days, weeks, and years I’ve already spent on WoW, but there is no way I’m going to look because if the total time played isn’t years, it’s undoubtedly months and months.
\nFor me, the act of purchasing and installing Vanilla was a multi-week agonizing process. I’d log into Battle.net, move my mouse cursor over the Subscribe Now button and sit there. Am I ready for this? Do you remember how much time you spent raiding Molten Core? How about Zul’Gurab? Years. Do you need more missing months?
\nBut I had to know. Having played most of the subsequent releases of WoW and watched how the game evolved, I wanted to see if the original spark, the unique hook, was still there. Sure, I was nostalgic, but was the grind going to be worth it?
\nIt is.
\nA Punishing Grind
\nEverything and I mean everything is a grind in Vanilla WoW whether it’s increasing your base level, your professions, and/or your secondary skills. Nothing comes from free. You must devote significant time to any attribute you want to grow, and that means finding resources and converting them into useful goods that you either use or sell.
\nI choose leather-working as one of my professions which pairs nicely with my other profession: skinning. Efficient, right? Skin the animals that I kill and turn those skins into useful materials. Sure, except this is Vanilla where drop rates for many items are anemic. Need five bear skins? Ok, go kill 50 or so bears. Where are these bears? Well, there’s a bunch in the north part of Darkshore which is a five-minute walk or I can head south where there are more bear clusters, but that’s more like a ten-minute walk. No fast travel, no mounts, I need to point my Night Elf south and walk down a forest path for ten minutes… in the rain.
\nYes, there are other quests to do on my long walk to the bear massacre, but these quests contain the same DNA as the rest of Vanilla. Nothing for free plus an enormous grind. Lengthy collection quests with frustratingly low drop rates. Oh yeah, did I mention that if I leave a level-appropriate area, I’m going to die? The moment I run into an over-leveled baddie, I’m dead. When this happens (and it will), I appear as a ghost at a usually not nearby cemetery where I am required to run back to my corpse where I will likely to die again at the hands (claws, paws, whatever…) of the same vastly overpowered baddie.
\nHow is any of this fun?
\nConspicuously Helpful and Painfully Kind
\nWoW is a profoundly complex puzzle, and puzzles are meant to be solved. My first week of grind in Vanilla was vastly more satisfying than my first week over a decade ago because I intimately know how this puzzle is constructed. I rolled a hunter because I am clear how a hunter works. I’m ranged damage, I need a pet as quickly as possible because my melee attack sucks, and I need to become adept at managing that pet so that I don’t die so I can avoid long ghost walks.
\nI am space constrained. Bags are worth their weight in gold, but I have no gold. I have bronze coins which with a ton of work will become silver coins which eventually will become gold coins. I’m going to need 100 of these hard to earn gold coins when I hit level 40, so I can purchase a mount and travel efficiently. I’m level 18 now. I have 22 silver and 81 bronze. I am forever away from 100 gold coins.
\nAnd then, standing there in the rain in Darkshore, a random Dwarf walks up next to me, opens the trade window, and gives me a bag. For free. No commentary, no requests, just a free bag in the middle of the forest. I bow. He walks off.
\nOh right…
\nNow, I’m standing outside of this cave, and it’s a cave full of quest-necessary mushrooms protected by fish-like creatures named Naga. I remember this cave from years ago because I spent hours of my life ghost walking back to this cave after being swamped by Naga. I did not know what I know now that just about everything is easier with others in WoW. Social connections are encouraged, so the moment I see a friendly player I request to join forces, and they accept. We competently charge into the cave and quickly overpower the Naga.
\nOh, right… people are helpful in this game.
\nWhen it became clear to me the I was going to play Vanilla, I searched the internet from remnants of my old guild, Liquid Courage because those humans were why I devoted time to this game. A bunch of gaming strangers building a helpful community. The reason the ten-year-old game was so familiar to me was because of those helpful mostly anonymous humans who took the time to explain how it worked, how to optimize my play, and how to work together to solve this punishing grind.
\nOh, right… people are helpful in this game. The community is what made this game great. Conspicuously helpful and painfully kind.
\nDarkshore is a Place… To Me
\nLiquid Courage is still a guild on Cenarius, one of the many realms on the latest World of Warcraft. I recently logged in quietly to see familiar names there. I found the guild on Facebook, too. Familiar names were now playing with children who were yet born the last time we charged into Zul’Gurab seeking the heart of Hakkar.
\nWhen I started playing WoW, there was no rain. It was an effect that was added in a later patch. I do remember the moment I first saw and heard the rain. I wasn’t in Darkshore; I was in the Arathi Highlands – a different continent. I don’t remember if I was level 60, yet, but I remember when it started raining. I was questing with a guild-members. Did we need spiders? Maybe?
\nIt just started raining, and we celebrated the digital rain.
"},"visual":{"url":"none"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/e31b3fcb-27f6-4f3e-b96c-53902586e366","label":"Weblogs"}]},{"keywords":["Advice","Development","Opinion"],"originId":"https://furbo.org/?p=2411","fingerprint":"1953a570","id":"6IpDVPJg6aqZjbDgAeUFNoin2kWUje5qGrfJkJShV7Q=_16d81299bad:4da11:18991ffa","author":"Craig Hockenberry","summary":{"direction":"ltr","content":"Luckily, I don’t have to use this kind of title often. But when I do, there’s a good reason: this year’s beta release cycle for all of Apple’s operating systems has been a mess. The months since WWDC in June have been a terrible experience for both customers and developers alike and the literal center […]"},"alternate":[{"href":"https://furbo.org/2019/09/04/icloud-clusterfuck/","type":"text/html"}],"crawled":1569830050733,"title":"iCloud Clusterfuck","published":1567623053000,"origin":{"streamId":"feed/http://furbo.org/feed/","htmlUrl":"https://furbo.org","title":"furbo.org"},"content":{"direction":"ltr","content":"Luckily, I don’t have to use this kind of title often. But when I do, there’s a good reason: this year’s beta release cycle for all of Apple’s operating systems has been a mess. The months since WWDC in June have been a terrible experience for both customers and developers alike and the literal center of the chaos was Apple’s iCloud syncing service.
\nFor us, it all started with customers reporting lost Linea sketches in their iCloud Drive. Initial investigations led to a common factor: all of the people affected had installed the iOS 13 beta release.
\nAnd when I say lost, I mean really lost. Entire folders were either gone or corrupted. Apple’s mechanism to recover deleted files was of no help. The customers with weird folder duplicates were the “lucky” ones.
\nWe couldn’t find any problems in our code and there was no information from Apple about problems with iCloud, so we did the only thing possible: we blocked people from using Linea on iOS 13. If a customer absolutely needed the app, we got them onto TestFlight after a stern warning about the real possibility of losing data.
\nA few weeks later, Apple finally indicated that there were some issues with iCloud and the beta release. In the same week, they released a public beta and sent out an email to customers encouraging them to try out iOS 13.
\nWe did our best to understand the situation and provide information to Apple, but it felt like we were tossing bug reports into a black hole. The most discouraging part was when we tried to open an incident with Apple Developer Technical Support (DTS). After writing up a detailed report, we were informed that they don’t support beta releases!
\nBy the time beta 6 rolled around, things were improving, but there were still isolated reports that the service might not be completely back in working order. Confirming the fixes on our end was impossible: the folks who’d encountered the previous data loss had no desire to mess with iCloud again.
\nNow it appears that the entire stack is getting rolled back and there won’t be new iCloud features in iOS 13 (at least initially.) I honestly think that’s the wisest course of action at this point. My only wish is that Apple would make an official statement.
\nNow that we’re past the worst of it, it’s time to think about why this whole episode happened and how it can be prevented in the future.
\nFolks don’t understand beta
\nApple’s biggest fuck up was a bad assumption about who is testing a beta release. In WWDC presentations and developer documentation, there’s always warnings about using the release on non-production devices and only with test accounts.
\nBut there are many folks that are just looking to get the new and shiny features. In past iOS beta releases, Apple hasn’t suffered too much from this because the early software was relatively stable. Maybe you got some dropped calls or bad battery life, but it was nothing too serious.
\nThese early adopters installed iOS 13 and expected a similar experience. They also weren’t using an iCloud test account, so any instability in the beta release propagated bad data to their other devices.
\nDevelopers have long known to unhook external drives when testing a new OS release. Shit happens, and that’s OK because it’s a beta and we expect a bumpy road. In fact, I currently have an external Photos Library that I can’t use in Mojave because it got upgraded when I booted into Catalina: it’s my dumb mistake and I have no complaints.
\nAnyone who’s not a developer, and hasn’t been burned by a bad OS, does not know the kind of trouble that lies ahead. It’s irresponsible for Apple to release a public beta with known issues in iCloud. It’s doubly egregious to then promote that release with an email campaign to customers. For a company that prides itself in presenting a unified front, it sure looks like the left hand doesn’t know what the right hand is doing.
\nDisconnect iCloud by default
\nThe solution to this situation is relatively simple, but with painful consequences. Apple needs to help unwitting customers by automatically disconnecting that external hard drive called iCloud.
\nIf a device is using an Apple ID that’s also being used on a non-beta device, then iCloud shouldn’t be allowed. If you install an iOS beta on your iPad, it doesn’t get to use any cloud services because it puts the data on your iPhone or Mac at risk.
\nOf course, once this restriction is put into place, Apple will quickly realize that no one is testing iCloud or anything that touches it. If a beta tester can’t have their contacts, synced notes, reminders, and appointments over the course of three months, they’re not going to be testing much. I’d also expect to see lots of guides on “How to Get Back on Normal iOS” around the web.
\nBut there’s a better, and even more unlikely solution…
\niCloud can’t be a beta
\nBecause it’s a service, iCloud doesn’t get to go into beta. It needs to be reliable all the time, regardless of whether iOS or any other platform is in beta test.
\nAs it is now, Apple is effectively telling you that your storage device will be unreliable for a few months. It’s like having a hard drive where the manufacturer tells you it won’t work well for ¼ of the year. Would you purchase storage with a caveat that “the drive mechanism may not work properly during the hot summer months”?
\nYou don’t see these kinds of issues with Google Drive or Dropbox because they don’t get a pass while an OS is being tested. Dropbox moved your folders from AWS to their own servers without you noticing even the slightest hiccup. Compare this to the last three months of iCloud in beta.
\nAnd yes this is a hard task: akin to swapping out the engines of an airplane in mid-flight. But data services, like hard drives, must work all the time.
\nWe’re well past that point with iCloud and it’s just going to get worse as Apple moves more of our data there. This time next year, will I suddenly not know which episodes I’ve watched on Apple TV? Or will I suddenly lose all my scores and achievements in Apple Arcade? Will I shoot myself in the foot with some cool new feature in iOS 14?
\nUnfortunately for Apple, breaking iCloud away from the OS isn’t a simple problem to solve. It’s not a technical challenge as much as it is an organizational one: a move from a functional form to a divisional one.
\nSo while we all dream of an OS release roadmap to ease the transition to new features, Apple needs to give some very close to attention to the underlying services that would enable that new way of working.
\nThe lack of substantive communication about iCloud’s problems this summer shows how far the company has to go in that regard. You can’t use a roadmap without letting people know about your successes and failures. There will be wrong turns, and you can’t continue on in silence when people depend on the service’s reliability.
\niCloud gives data a bad name
\nAs a developer whose job requires frequent beta installations, this year’s problems with Apple’s service has brought something clearly into focus: as much as I love the functionality that iCloud provides, I cannot put my valuable data at risk.
\nAnd you can bet your ass that I have a new backup strategy for anything that’s in iCloud Drive. (Hint: rsync and ~/Library/Mobile Documents.)
\nAs an Apple shareholder, I also worry about how these failures will damage the iCloud brand. Apple’s growth hinges on services and when they get a bad name, the stock price can only go one way. There is only one brand that people think of when you mention “click of death”.
\nI sincerely hope that Apple can address these issues, because I love the idea of services that focus on simplicity and privacy. I know my last clusterfuck memo got some notice within the company, and I hope this one does as well.
"},"visual":{"url":"none"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/e31b3fcb-27f6-4f3e-b96c-53902586e366","label":"Weblogs"}]},{"keywords":["Development"],"originId":"https://furbo.org/?p=2384","fingerprint":"6abb6e16","id":"6IpDVPJg6aqZjbDgAeUFNoin2kWUje5qGrfJkJShV7Q=_16d81299bad:4da10:18991ffa","author":"Craig Hockenberry","summary":{"direction":"ltr","content":"We recently started updating our macOS apps for Catalina: so far there have been very few issues with APIs and frameworks. The biggest hurdle has been the new notarization process that’s required for apps signed with a Developer ID: customers will be unable to download and launch your product easily until this step is completed. […]"},"alternate":[{"href":"https://furbo.org/2019/08/16/catalina-app-notarization-and-sparkle/","type":"text/html"}],"crawled":1569830050733,"title":"Catalina, App Notarization, and Sparkle","published":1565997404000,"origin":{"streamId":"feed/http://furbo.org/feed/","htmlUrl":"https://furbo.org","title":"furbo.org"},"content":{"direction":"ltr","content":"We recently started updating our macOS apps for Catalina: so far there have been very few issues with APIs and frameworks. The biggest hurdle has been the new notarization process that’s required for apps signed with a Developer ID: customers will be unable to download and launch your product easily until this step is completed.
\nNotarization involves an extra step in your build process: you upload an archived binary to Apple’s server with Xcode’s Organizer window and a short time later, you can export the binary. If you’ve automated your build process, you’ll need to make changes to your scripts to accommodate this new manual step. Apple’s documentation explains the process well.
\nBefore you can notarize the app, you’ll need to enable the hardened runtime in the target’s Capabilities panel. After flipping the switch you’ll see a array of exceptions and access permissions. You’ll want to survey this list carefully: for one product we needed Apple Events, for another Location was required.
\nThings start to get tricky when you go to upload the binary: if you’re using Sparkle, it’s probably been codesigned without the hardened runtime, so you’ll immediately see an error.
\nSparkle Without a Sandbox
\nHow you deal with this error depends on which of the Sparkle versions you’re using. If your app isn’t sandboxed, your life will be a bit simpler because there are fewer things you’ll need to sign manually.
\nAfter the target’s Copy Files build phase where the Sparkle.framework is moved into the application package, you’ll need to create a new Run Script step: I called ours “Sign Frameworks”. The script looks like this:
\n\nLOCATION="${BUILT_PRODUCTS_DIR}"/"${FRAMEWORKS_FOLDER_PATH}"\nIDENTITY=${EXPANDED_CODE_SIGN_IDENTITY_NAME}\n\ncodesign --verbose --force --deep -o runtime --sign "$IDENTITY" "$LOCATION/Sparkle.framework/Versions/A/Resources/AutoUpdate.app"\ncodesign --verbose --force -o runtime --sign "$IDENTITY" "$LOCATION/Sparkle.framework/Versions/A"\n\nThe key part in this step is the -o runtime. The codesign manual page describes this flag as:
\n\nOn macOS versions >= 10.14.0, opts signed processes into a hardened runtime environment which includes runtime code signing enforcement, library validation, hard, kill, and debugging restrictions. These restrictions can be selectively relaxed via entitlements. Note: macOS versions older than 10.14.0 ignore the presence of this flag in the code signature.\n
\nThe good news here is that the build changes we’re making won’t affect your app when it runs on an older version of macOS.
\nSparkle in a Sandbox
\nIf your macOS app is in a sandbox, you’ll be using the version that relies on XPC services to perform the update. Like everything else in your application package, these will need to be signed correctly before you can submit your app for notarization.
\nThe “Sign Frameworks” build phase should look like this:
\n\nLOCATION="${BUILT_PRODUCTS_DIR}"/"${FRAMEWORKS_FOLDER_PATH}"\nIDENTITY="${EXPANDED_CODE_SIGN_IDENTITY}"\n\ncodesign --verbose --force -o runtime --sign "$IDENTITY" "$LOCATION/Sparkle.framework/Versions/A/Resources/AutoUpdate"\ncodesign --verbose --force --deep -o runtime --sign "$IDENTITY" "$LOCATION/Sparkle.framework/Versions/A/Resources/Updater.app"\ncodesign --verbose --force -o runtime --sign "$IDENTITY" "$LOCATION/Sparkle.framework/Versions/A"\n\nYou’ll also add a new Run Script build phase just before the XPC Services are embedded in your application package. Since you’ll only need to do this for release builds, the script looks like this:
\n\nif [ "${CONFIGURATION}" = "Release" ]; then\n $PROJECT_DIR/Sparkle/bin/codesign_xpc "Developer ID Application" $BUILT_PRODUCTS_DIR/*.xpc\nfi\n\nBut wait, we’re not done yet! You’ll also need to update the codesign_xpc Python script with the -o runtime flag. It looks like this when you’re done:
\n\ndef _codesign_service(identity, path, entitlements_path=None):\n command = ["codesign", "-f", "-o", "runtime", "-s", identity, path] + ([] if entitlements_path is None else ["--entitlements", entitlements_path])\n log_message(" ".join(map(sanitize, command)))\n ...\n\nYou’re Not Done Yet
\nAt this point, you should be able to do a build where everything in your app is using the hardened runtime. It’s more likely that you’ve had some kind of issue along the way: this Apple document helped get me over the rough patches. (Thankfully, I didn’t have to write it this time around.)
\nAfter the notarization upload completes, you’ll see “Uploaded to Apple” in the organizer, then after a few minutes you’ll get an email and Xcode notification that your app is “Ready to distribute”. In the righthand panel underneath “Distribute App”, you’ll see that the “Export Notarized App” button is enabled and can be used to place the signed package anywhere on your Mac for further processing.
\nIn our case, we had to split up the build scripts into two parts: previously we had a single script that did the build, signed it with the Developer ID, and then created an appcast. Sparkle’s XML file is now created with a separate script that also prepares the release to be checked into our repositories.
\nOne final note: these instructions are based on Xcode 10, which is currently the only development tool that can be used to submit an app for notarization or the Mac App Store. Before we figured that out, we found that Xcode 11 does a better job passing along the -o runtime flag during a framework’s Code Sign On Copy. It’s likely that all this work you just did will only be needed for a few months. Sigh.
"},"visual":{"url":"none"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/e31b3fcb-27f6-4f3e-b96c-53902586e366","label":"Weblogs"}]},{"originId":"http://david-smith.org/blog/2019/09/26/moon-plus-plus","fingerprint":"d717143d","id":"37MS5KwinfzXRHFFH8onqsLrpe3pbr+gQeC/KCTSLiw=_16d81299335:4da0f:18991ffa","updated":1569510120000,"alternate":[{"href":"http://david-smith.org/blog/2019/09/26/moon-plus-plus/","type":"text/html"}],"crawled":1569830048565,"title":"Moon++, a better lunar complication","published":1569510120000,"origin":{"streamId":"feed/http://david-smith.org/atom.xml","htmlUrl":"http://david-smith.org/","title":"David Smith, Independent iOS Developer"},"content":{"direction":"ltr","content":"This app started life as so many of my apps do…from a frustration that just got under my skin.
\nI just received my new Series 5 Apple Watch. While reading reviews for it, many reviewers were using the moon complication in their product shots. I think this is because it is the default complication for the new California watch face. Anyway, I liked the overall aesthetic of it and so tried it out myself.
\nI went outside and looked up at the moon in the sky. This is what I saw:
\n
\nThen I looked down at my wrist and saw this:
\n
\nThis bothered me more than it should. The two don’t really look the same at all. On my wrist is a powerful computer, capable of precisely locating itself on the surface of the earth (and indeed the solar system!)…but it isn’t showing me the correct moon.
\nSo I had to fix it…
\n
\nThe result is Moon++.
\nMoon++ seeks to provide a highly accurate, visually pleasing indication of what the moon looks like right now, right where you are. My goal was to make it so that if you look down at your wrist and then up into the sky the images you see should match.
\nBuilding it has been a fun little adventure into astronomy. The moon image I generate is adjusting itself for all manner of astronomical factors. I got to learn about such fun terms as: parallactic angle, libration, and lunar terminator.
\n
\nIt includes a robust set of complications for every watch face type. You can choose between either a visually rich ‘actual’ appearance or a more simplified version, to fit your own personal style.
\n
\nWithin the actual app itself it displays the current location of the moon in the sky (azimuth and elevation) along the horizon. If you rotate the Digital Crown you can shift forward and backwards through time.
\nAdditionally, on the new Series 5 Apple Watch, which is equipped with a compass, you can have Moon++ orient itself towards the actual moon to make it easier to locate the moon in the sky.
\nI hope you enjoy this little utility, I really enjoyed making it.
\nIt is $0.99 in the App Store.
\n
\nA Press Kit for the app is here.
\n
\nTechnical Note:
\nThis is my first app built using SwiftUI. It is really cool to see how interactive and lively you can now make an Apple Watch app. The compass mode feature especially wouldn’t really have been practical or performant to build using WatchKit.
\nAlso, it is an Apple Watch only app, which means I didn’t have to build a hosting iPhone app.
\n
»"},"visual":{"url":"http://www.blogcdn.com/www.engadget.com/media/2013/10/galaxygear-lead.jpg","width":619,"height":411,"contentType":"image/jpeg"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/e31b3fcb-27f6-4f3e-b96c-53902586e366","label":"Weblogs"}]},{"originId":"http://david-smith.org/blog/2019/09/13/seamless-light-complications","fingerprint":"cd8567d7","id":"37MS5KwinfzXRHFFH8onqsLrpe3pbr+gQeC/KCTSLiw=_16d81299335:4da0e:18991ffa","updated":1568388600000,"alternate":[{"href":"http://david-smith.org/blog/2019/09/13/seamless-light-complications/","type":"text/html"}],"crawled":1569830048565,"title":"Seamless Light Complications","published":1568388600000,"origin":{"streamId":"feed/http://david-smith.org/atom.xml","htmlUrl":"http://david-smith.org/","title":"David Smith, Independent iOS Developer"},"content":{"direction":"ltr","content":"I really like the new Meridian face that arrived in the watchOS 6 GM. It is clean, simple but still provides ample opportunity for customization. Over the summer I’ve been rocking the California face (with Arabic numerals because its default mixed Roman numerals hurt both my head and heart). But the California face only has extensive complication options in the corners which isn’t my preferences. I like the face itself going to the edge of the display, it is one of the things I like most about the Series 4/5 display, that it really fills the face.
\nThere is, however, one thing I’m now really wishing for with the Meridian face…the ability to make seamless complications for its light face options.
\nFor the dark face variants, we already have this. You have white text on a black background that blends seamlessly into the background of the watch.
\n
\nIncreasingly Apple is providing options for us to have non-black watch faces, which I think creates a delightful bit of character and delight for the Apple Watch. These started with the Infograph faces and now continue with both California and Meridian. However, sadly we don’t yet have a way to make complications that can blend seamlessly into the these light colored backgrounds. The best I can currently do is something that looks kinda like this:
\n
\nWhich isn’t awful, but really interrupts the full effect that would be possible if the complication blended seamlessly into the background. Apple already allows this for the “Digital Time” complication so I know it isn’t completely out of the question. Here is an altered version of the above screenshot removing the ring around my complications.
\n
\nDoesn’t that look great!
\nSo this is my little feature request for apple (filed as FB7264371), I’d love to see a way to provide seamless complications for these colored backgrounds. Fingers crossed!
\nNote: The font used in my custom date and weather complications is Redbird, which is my current favorite for display on an Apple Watch face. It just looks at home on this device.
\n
»"},"visual":{"url":"http://www.blogcdn.com/www.engadget.com/media/2013/10/galaxygear-lead.jpg","width":619,"height":411,"contentType":"image/jpeg"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/e31b3fcb-27f6-4f3e-b96c-53902586e366","label":"Weblogs"}]},{"originId":"http://www.caseyliss.com/2019/9/19/stalman-podcast","fingerprint":"a907e8ac","id":"fkFLg490GSKODJZCYxuxuSDw5utmxlHxrMJQGyyxmh0=_16d8129898d:4da0d:18991ffa","author":"Casey Liss","summary":{"direction":"ltr","content":"Last night I recorded a podcast with my pal Tyler Stalman on his\nshow, The Stalman Podcast. Tyler received review units of the\niPhone 11, iPhone 11 Pro, and Apple Watch Series 5. After a whirlwind\nday of him testing the phones, Tyler made some time to sit down with\nme to discuss them.
\nIn this episode, I turned the tables on Tyler and interviewed\nhim. As a professional photographer, Tyler naturally had a lot of\nthoughts about the new two-camera system on the 11 and the three-camera\nsystem on the 11 Pro.
\nI had a ton of fun recording this with Tyler, and for those of you\nwaiting in line tomorrow, or debating buying a new iPhone, this episode\nis for you.
\nRead on Liss is More
"},"alternate":[{"href":"https://www.stalmanpodcast.com/64","type":"text/html"}],"crawled":1569830046093,"title":"Appearance: Stalman Podcast 64","published":1568942100000,"origin":{"streamId":"feed/http://www.caseyliss.com/rss","htmlUrl":"https://www.caseyliss.com","title":"Liss is More"},"visual":{"url":"https://cdn.vox-cdn.com/thumbor/06e5FJWgUfUSmDaPJIEZoGF1XOs=/0x68:2040x1136/fit-in/1200x630/cdn.vox-cdn.com/uploads/chorus_asset/file/10378819/DSCF3031.jpg","width":1200,"height":628,"contentType":"image/jpeg"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/e31b3fcb-27f6-4f3e-b96c-53902586e366","label":"Weblogs"}]},{"originId":"http://www.caseyliss.com/2019/8/27/family-feud","fingerprint":"8d3ef9eb","id":"fkFLg490GSKODJZCYxuxuSDw5utmxlHxrMJQGyyxmh0=_16d8129898d:4da0c:18991ffa","author":"Casey Liss","summary":{"direction":"ltr","content":"Just over five years ago, Myke and I announced Analog(ue). It doesn’t\nfeel like it was that long ago, but here we are. Analog(ue) is older than both\nof my children; Myke and I have released 163 episodes of the show. Not bad for\nsomething we thought would peter out after ten or twenty episodes.
\nTo celebrate Analog(ue)'s Relay’s fifth anniversary, over twenty hosts\njoined Myke and Stephen in San Francisco to record a live game of\nFamily Feud. It was a blast, and probably the zaniest podcast I’ve\never been a part of.
\nThe whole episode is up on the Connected feed, if you’d like to have a\nlisten. There may be video in the future, but no guarantees. If there is, I’ll\nlink it here.
\nRead on Liss is More
"},"alternate":[{"href":"https://www.relay.fm/connected/257","type":"text/html"}],"crawled":1569830046093,"title":"Appearance: Connected 257","published":1566916200000,"origin":{"streamId":"feed/http://www.caseyliss.com/rss","htmlUrl":"https://www.caseyliss.com","title":"Liss is More"},"visual":{"url":"https://cdn.vox-cdn.com/thumbor/06e5FJWgUfUSmDaPJIEZoGF1XOs=/0x68:2040x1136/fit-in/1200x630/cdn.vox-cdn.com/uploads/chorus_asset/file/10378819/DSCF3031.jpg","width":1200,"height":628,"contentType":"image/jpeg"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/e31b3fcb-27f6-4f3e-b96c-53902586e366","label":"Weblogs"}]},{"originId":"https://allenpike.com/2019/multiply-your-time","fingerprint":"99105153","id":"r0GuC4sgAzEOyudB9cuXyJaa1HYa9lXs9s0Sf3Zd858=_16d8128aefe:4da03:18991ffa","updated":1567278610000,"author":"Allen Pike","alternate":[{"href":"https://www.allenpike.com/2019/multiply-your-time","type":"text/html"}],"crawled":1569829990142,"title":"Multiply Your Time","published":1567278610000,"origin":{"streamId":"feed/http://www.allenpike.com/feed/","htmlUrl":"https://www.allenpike.com/","title":"Allen Pike"},"content":{"direction":"ltr","content":"One of my most hated things is when someone over-fills a garbage bag.
\nYou see, an almost-full garbage bag is a small task to deal with – you just close it up and pull it out. But an over-full garbage bag is a problem. Suddenly, you can’t just pull it closed anymore – dealing with the bin now involves biohazardous juggling, something nobody is inclined to do anytime soon. The resulting additional procrastination leads to, in most cases, an unstoppable garbage snowball that eventually destroys humanity itself.
\nEverybody has their irrational pet peeves. As long as I can remember, this has been one of mine. For years I always tried to prioritize the task of emptying the bin before it got over-filled. Sometimes I succeeded, but other times I failed, and a stupid garbage can would put me on tilt.
\nOne day, when I attempted the task of emptying the bin, I found it over-full. And instead of emptying it, I left. I left for Home Depot, on a different task that would change how I thought about prioritization forever: I bought a smaller garbage bin.
\n
\nIt may be obvious to you, but for years I ignored the root cause of my age-long grief. I did nothing about the thing that actually enabled people to pile in more garbage than the bag could hold: the bin was bigger than its bag. By buying a smaller bin, the same bags simply couldn’t be over-filled. Even the most precariously over-filled bin could be wrapped up swiftly and neatly, using the specially reserved portion of bag draped on the outside of the bin, ready and waiting for duty.
\nIt was glorious.
\nThe quietly worthwhile
\nThere is a lot of stuff you should get done.
\nOr, more accurately, there is a lot of stuff you feel like you should get done. My OmniFocus tracks 860 actions that, at one time, I felt like I should do. Many of them won’t get done. But there are some things in there that I truly should do, and if I do them, I’ll be very glad.
\nThe question is: which ones?
\nSome actually-important things are urgent. If you don’t renew your passport, you can’t go on your upcoming trip. If you don’t empty the garbage before garbage day, you’ll be stuck with old garbage for two weeks. The task needs to get done, you need to do it, so you just gotta do it. It’s kind of obvious, because it’s both urgent and important.
\nMore interesting, though, is the non-urgent stuff that is nevertheless very worthwhile. The tasks that are easy to defer, tempting to procrastinate, but actually more important than the supposedly urgent tasks: the smaller garbage bins, waiting to be bought.
\nSo, when I’m looking at things I should do, I keep an eye out for certain kinds of tasks: work that isn’t urgent, but can multiply time. Things that could pay off for months or years to come.
\nIn particular, I try to consider if I can:
\n\n- Automate. Can you spend 1 hour simplifying or automating something that would then save yourself 5 minutes a week? If so, then I have an incredible investment opportunity for you: you can invest time – the most precious resource we have – at a 430% annual interest rate. The busier I get, the harder it is to get around to tackling these automation and simplification tasks, but the more worthwhile it is.
\n- Teach. If there’s somebody who’d be willing to do this task in the future, but they don’t know how yet, then you have a big opportunity. The long-term payoff for teaching someone how to do a task can be massive. Even in the case of successfully handing something off only for it to “boomerang” back later, having taught still improves your understanding of the work and makes you a better teacher for the next attempt. If you’re in a leadership position of any kind, you don’t have the time not to be teaching people.
\n- Calm. Like many people, I have a certain amount of tolerance for frustration and stress in a given day. At a certain point further annoyances, even small ones, cause disproportionate reactions and sap energy from me and those around me. That’s why I find it useful to prioritize fixing anything that is a persistent aggravation or stressor. In OmniFocus, I keep an ongoing project “Make Life Calmer”. Doing work today that will make tomorrow calmer and more focused is a great investment, and will let you use your future “stress budget” on something more meaningful than the fact the damn kitchen drawer is stuck closed again because the saran wrap and aluminium foil boxes got pushed on top of each other again and god DAMN it why do I have three different sets of measuring cups in here augghhhh.
\n- Improve. It is extremely easy to procrastinate making yourself better. It takes motivation to build new habits, learn how do to something “right”, or to address a longstanding problem. But this work is profoundly worthwhile, since small investments here can have huge payoffs. After 35 years of using a computer like a dumb animal, I’m finally learning how to sit, type, and work in a way that doesn’t permanently injure my wrists and shoulders. Reading books about RSI or learning better posture aren’t necessarily my idea of fun, but I’m going to benefit from the investment for decades to come.
\n
\nDiamonds in the rough
\nNext time you’re cleaning up your todos, considering a new goal or theme, or just feeling over-busy, consider how you can be multiplying your time. What things, once done, will have an impact that pays off for years?
\nWhat is your smaller garbage bin?
"},"visual":{"url":"http://cdn0.sbnation.com/entry_photo_images/9193021/3462607995_150a6b2624_z_large.jpg","width":630,"height":420,"contentType":"image/jpg"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/e31b3fcb-27f6-4f3e-b96c-53902586e366","label":"Weblogs"}]},{"id":"K2yEyJkJAlTYIjP5i2D9ccp/wbMYzpHnabsacInQgcs=_16d7e95f988:1fc4:90d684ff","keywords":["digital-publishing","newspapers","france","search-engine-optimizati","media"],"originId":"https://medium.com/p/948b24fd555c","fingerprint":"ea8aeae3","content":{"content":"#MN_Briefing 09.29.19
by Frederic Filloux
Google responding to the French law on snippetsIn today’s briefing: Google on the warpath with French publishers | Le Monde facing crucial choices for its future | The New York Media/Vox deal | Some news about Deepnews | Great long forms.
1. Google fires a warning shot at EU publishers
TThe Facts. Google chose the hard way when faced with the decision on how to comply with France’s new copyright law, set to come into force in late October. The law states that publishers are entitled to be paid by online services that display snippets of articles. It is the result of intense pressure on behalf of French publishers.
Last Wednesday, Google announced that, in order to be fully compliant with the new French law, it will simply stop showing snippets below headlines, as well as thumbnails.
Here is an abstract of Google’s statement (full text in FR and ENG here), emphasis mine:
At the moment, when we display news results, we show a headline, which links directly to the relevant news site. For some results, we also show you a short preview of the article, such as a few lines of text (also known as a “snippet”) or a small “thumbnail” image. Together, these headlines and previews can help you decide whether a result is relevant to your search, and whether you want to click on it.
When the French law comes into force, we will not show preview content in France for a European news publication unless the publisher has taken steps to tell us that’s what they want. This applies to search results across Google services.
Why it matters.
1) French publishers have a lot to lose in the process. To understand the visible impact of Google’s decision, here is the same set of news, collected yesterday morning with a simple query “Jacques Chirac” (the former French President who died last week), in Google search. Assuming none of the publishers mentioned below will take the steps to opt-in back to the former system of snippets (or snippets + thumbnail), the impact will be significant:

Maintaining the volume of traffic sent back to publishers from search results will require a united front in which no one publisher accepts going back to the headline + snippet system, and/or the adoption of clear and straightforward headlines. Otherwise, the loss in traffic could be significant.
2) Google’s reaction equals to a shot across the bow of EU publishers: It says: “Do not push your governments to pass such kinds of law, or you will face the consequences”. Google’s position has the merit of consistency. Over the last five years, the search giant stood by its tune: “We won’t pay for snippets. Ever”. The news should not, therefore, come as a surprise for the French publishers. But, as an industry addicted to subsidies, supposedly protected by the EU as the Daddy and the French government as the Mummy, they hoped for a miracle which didn’t happen.
The lingering questions that I will cover in a future Monday Note:
1 . How French publishers will react to this, besides besieging the Ministry of Culture, who already slammed Google’s decision as unacceptable (press release in FR here)?
2 . It varies from one publisher to another, but Google News and Google Search might count for as much as 30 percent of the traffic a news site is getting. French publishers will still be present in SERPs (Search Engine Return Pages), but, as a default setting, their presence will be reduced. What will be the impact on their traffic? How many will opt-in to go back to the original system? Will they remain united? (Of course not).
3 . Is there some room for negotiations between Google on one side and the French government and the publishers on the other?
4 . Why does the issue of paid-for-snippets seem to be so specific to the EU and more specifically French publishers?
5 . If we take into account, on one hand, the value of the clicks sent to the publishers + the amount of funding direct and indirect injected by Google into the news sector (hundreds of millions of dollars), and on the other hand, the benefits for Google of inserting news elements in the SERPs, is the news industry really at a disadvantage?
Background: Past Monday Notes that cover the issues:
2. Follow-up on Le Monde’s saga: tough choices, hard consequences
LThe apparent good news: all shareholders of Le Monde agreed last week to sign a pact that will give to the staff’s representatives the right to veto an unwanted investor. At the same stroke, they decided to “consider the possibility” of a foundation. So far, only Xavier Niel, the telecom mogul who calls the shots here, is pushing in this direction. Despite an acceptable write-off, he will appear as the savior of Le Monde. Other stakeholders are not on board: Matthieu Pigasse, who narrowly escaped a personal bankruptcy, is about to make a good deal; Madison Cox, who manages Pierre Bergé estate, wants to sell his shares for a profit, and the newcomer, Daniel Kretinsky, who teamed up with Pigasse, is a patient man with large ambition.
Why it matters. Le Monde is facing a crucial moment in its existence. Nine years ago, the paper was on the verge of bankruptcy. Today, it harbors good journalism, even occasional strokes of extraordinary reporting. Financially speaking, Groupe Le Monde is well-managed but faces an uncertain/negative outlook for the future, as all insufficiently diversified media companies do.
Now the choice is this:
a) Scenario A: Le Monde engages in a decisive development strategy with a comprehensive investment in technology to improve the performances of its core assets (the newspaper and magazines) and opts for diversification with acquisitions in services and e-commerce. The goal would be to double its current €300 million revenue in a few years, expand its footprint in France and maybe in Europe. That’s a realistic option, which can’t be done with an ownership structure that is leaning toward a non-profit.
b) Scenario B: Le Monde folds itself in a foundation-like system, fencing its ownership for good. The short-term will be nice: predatory investors will be kept at bay, management will be groomed inside the company — no one from the outside world will dare to compete for a CEO or an executive editor position knowing that they can be vetoed by the kolkhoz. It will be like a global airline deciding that it will never seek talent from abroad. If the fundamentals are fine, the company will remain within its cozy perimeter with a decaying genetic talent pool and a business that barely survives.
But most likely, fundamentals will degrade, with a continual depletion on the advertising side, a subscription fatigue and a market penetration that will plateau (unlike any English-speaking media which enjoys a growing global pool of educated readers). In less than ten years, Le Monde will be back to begging, with a déjà vu “bail-me-out-or-I-die” plea.
Background: Past Monday Notes on the subject:
3. New York Magazine + Vox: a good deal, by the numbers
NA great, iconic magazine combined with a powerhouse in digital news: that’s the New York Magazine and Vox Media deal. Altogether, they will operate 14 digital properties with a combined reach of 125 million unique visitors:

The all-stock deal values New York Media LLC at $105 million, a nice markup compared to the $55 million paid by the late famous investor Bruce Wasserstein in 2004 when he bought the company from Rupert Murdoch. According to the WSJ, NY Media’s revenue grew by 40% in the last 12 months but it lost $15 million in 2018. The magazine has been particularly good at segmenting its digital audience over several, remarkably efficient, entities. New York Media is getting 12 percent of the new venture that will retain the name of Vox.

As for Vox Media, it is valued at $750 million in the operation, a severe discount compared to a valuation of $1 billion in 2015. It makes a small profit on a $185 million revenue. It too has a segmented a valuable audience (78 million UVs). It is a super agile company, which built is own CMS (Chorus) and its own advertising production studio.
Why it matters. Despite its 44 magazine awards, the 51-year-old NY Mag would not have been attractive for a young digital native company if it hadn’t been for its profound modernization and sizable investment in digital operations. The Vox/NY Mag merger is the perfect proof that the magazine industry is not doomed.
4. My long forms of the week
Two great pieces from the New Yorker I read during a trip to Germany:
Can a Burger Help Solve Climate Change?
Eating meat creates huge environmental costs. Impossible Foods thinks it has a solution.
This piece is just incredible in its thoroughness, density, precision as well as its narrative structure. I take it as an example in my journalism classes.
Paging Dr. Robot.
A pathbreaking surgeon prefers to do his cutting by remote control.
I have been following Intuitive Surgical for a long time. The firm makes the extraordinarily complex DaVinci robot — which is, for a large part, the future of medicine. This piece is the best on the topic.
5. News about Deepnews
XTwenty issues of the Deepnews Digest have given us pretty good insight about the potential of lightweight, automated newsletters. Deepnews Digest is built on a different news-related topic every week. It gathers a large number of articles across 400 carefully vetted sources, with a final selection made by our proprietary algorithm, the Deepnews Scoring Model. The whole process is 95 percent automated, except for the writing of the introductory text and the removal of a few parasite links. Our excellent editor, Christopher Brennan, spends less than an hour each week to do the job.
Now, we want to develop a commercial version of the Deepnews Digest to cover specific sectors with a series of vertical, paid-for publications. The idea is to identify markets worth serving and to quickly build products, easy to test, launch and evaluate. Although we have an idea of where to go, we are quite interested in hearing from you about the sectors we should cover:
Please, take five minutes to complete this questionnaire.
Your opinion matters a lot to us.
See you next week.
frederic.filloux@mondaynote.com


#MN_Briefing 09.30.19 was originally published in Monday Note on Medium, where people are continuing the conversation by highlighting and responding to this story.
","direction":"ltr"},"title":"#MN_Briefing 09.30.19","updated":1569786960272,"author":"Frederic Filloux","alternate":[{"href":"https://mondaynote.com/mn-briefing-09-30-19-948b24fd555c?source=rss----c537d80ed0a---4","type":"text/html"}],"crawled":1569786821000,"published":1569786821000,"origin":{"streamId":"feed/http://www.mondaynote.com/feed/","title":"Monday Note - Medium","htmlUrl":"https://mondaynote.com?source=rss----c537d80ed0a---4"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/e31b3fcb-27f6-4f3e-b96c-53902586e366","label":"Weblogs"}]},{"id":"qxI9mPUQaH2N8pL2LRpLlzIW5hw1iiQKLdJmLkGau/I=_16d7e4a6db0:1f34:90d684ff","keywords":["User Interface","VR/AR"],"originId":"http://jnack.com/blog/?p=9310","fingerprint":"15a6ef0f","content":{"content":"Looks lit.
\n\n(Tangential reminder: You can build hand tracking into your mobile app right now using tech from my team.)
\n
\n[YouTube]
","direction":"ltr"},"title":"Oculus Quest adds hand tracking","author":"jnack","summary":{"content":"Looks lit. (Tangential reminder: You can build hand tracking into your mobile app right now using tech from my team.) [YouTube]","direction":"ltr"},"alternate":[{"href":"http://jnack.com/blog/2019/09/29/oculus-quest-adds-hand-tracking/","type":"text/html"}],"crawled":1569781870000,"published":1569781870000,"origin":{"streamId":"feed/http://jnack.com/blog/?feed=rss2","title":"Nackblog","htmlUrl":"http://jnack.com/blog"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/e31b3fcb-27f6-4f3e-b96c-53902586e366","label":"Weblogs"}]},{"id":"K2yEyJkJAlTYIjP5i2D9ccp/wbMYzpHnabsacInQgcs=_16d7d29c728:1fc3:90d684ff","keywords":["tesla"],"originId":"https://medium.com/p/29fd3b55c5b4","fingerprint":"e29fa3bb","content":{"content":"by Jean-Louis Gassée
With its just-announced Taycan 4-door sedan, Porsche got tongues wagging. Ultra-fast at the Nürburgring, ultra-refined — and ultra-expensive — it’s a clever entry in the EV race, one that some see as a threat to Tesla.
Courtesy WikipediaEnding months of speculation, Porsche picked Germany’s Frankfurt Auto Show (officially, the International Motor Show) to unveil the Taycan, the automaker’s entry in the Electric Vehicle race. To make the Taycan’s unveiling even more print- and click-worthy, weeks before the Frankfurt intro, Porsche headed to the famed Nürburgring track where the Taycan broke the 4-door EV record (video inside) with a 7 minute, 42 second lap.
For Monday Notes readers who aren’t into carnography, the legendary 20.8 km (12.9 mi) Nürburgring track has become a reference venue where automakers and tourist drivers attempt to break speed records in various categories.
(A brief historic digression: the first vehicle to break the 100km/h (62 mph) barrier was an electric car, yes, in 1899, La Jamais Contente (The Never Satisfied) managed to exceed 105km/h (65.8mph). This was the last time an EV drove faster than a gasoline-powered car.)
Before the Taycan, the Volkswagen ID R, a cousin EV in the Volkswagen Group, managed a 6:05 lap, an impressive feat but one that, unlike the Taycan’s, doesn’t belong in the Production Vehicles category. Earlier in the year, a modified ID R broke the Pikes Peak Hill Climb record.
The Volkswagen group, or VW AG, is a colossus that includes Porsche, Audi, Bugatti, and many others. It surpassed Toyota as the world’s largest auto maker by sales in 2016 and seems to have held the title since. By shattering EV speed records at historic venues, VW AG wants everyone to know that they’re not only big, they’re dead serious about their EVs, and about owning the performance crown in the category.
Furthermore, the giant German auto group has clearly decided to take the very high end of the production EV category. The Taycan starts at a theoretical $150K but, in Porsche tradition, it will easily exceed $200K in more desirable configurations.
For years, automakers have complained about the impossibility of generating any kind of profit by making EVs. Most of the models we’ve seen so far have either been “halo cars” meant to burnish a maker’s EV bona fides, or, as in the case of Fiat Chrysler electric Fiat 500, a way to gain CO2 emission credits. As an example, at the Frankfurt Auto Show mentioned above, Jaguar let it be known that its iPace had done its job and wouldn’t be iterated in the near term. BMW said very much the same about its carbon-fiber i3.
For “real” EVs, meaning models produced in hundreds of thousands or more per year, we must proceed with caution: Look at Tesla hemorrhaging $24B in free cashflow since 2011…
With this in mind, one can look at Porsche’s Taycan as a doubly cautious move: a very expensive vehicle, produced in modest quantities. In 2018, Porsche sold about 250K cars, with the Macan and Cayenne leading the way with 86K and 71K units, respectively. With the evergreen 911 selling about 35K units, one could expect Taycan 2020 sales to be in a low 10K multiple. A fine addition to the very profitable Porsche business — but not an invasion.
Unsurprisingly, the kommentariat was prompt to position the Taycan as a Tesla rival, a killer, even. Elon Musk helped by announcing an attempt to outdo the Taycan’s Nürburgring’s time “any day now”, perhaps with a new three engine configuration. Rumors aside, nothing so far. No one should second guess Musk’s ability to promote the Tesla brand (and himself), nor should we underestimate the value of the Nürburgring crown, but a Tesla-killer the Taycan isn’t.
To start with, there’s scale. Give or take a few thousand in the end-of-quarter rushes, Tesla will approach 400K deliveries this year and get close or exceed 500K units in 2020 when the company’s Gigafactory 3 comes on line in China (assuming, of course, that the economy is willing and investors continue to feed cash into Tesla).
Second, Tesla and Porsche operate in different price ranges. The Model 3, Tesla’s best selling car, now costs less than $50K; the Model S is less than $100K and the Model X is a bit more. There’s a big jump up to the Taycan’s $150k+ minimum price tag, certainly far too large for a faithful Model 3 adherent, and probably too big for even a Model X owner.
That said, many Porschephile connoisseurs think the real Tesla killer will be an EV Macan. The gasoline powered Macan is a best-seller today, and could be a best-seller tomorrow as a new, designed-from-the-ground-up EV sporty SUV that will compete more directly with the still-gestating Model Y Tesla SUV.
But a more immediate threat to Tesla is likely to come from elsewhere in the VW AG product portfolio. At the Frankfurt show, Volkswagen announced the ID.3, a car that many see as the EV equivalent of the iconic Golf, a vehicle that managed to span 45 years in seven generations without losing its soul. Production of the ID.3 is slated to start this coming November with deliveries for existing reservations in the Summer of 2020, albeit limited to 30K units for the “1ST” edition (or so we think…it isn’t entirely clear). The price is likely to exceed 40K€ (close to $50K).
Stepping back a bit, while the ID.3 should offer some competition in the Model 3 price range, it isn’t yet a full-bore assault on Telsa. Today, the Model 3 is a top seller in several European countries and neither VW nor Porsche threaten this position for the near-to-medium term, meaning 2020. After next year, things could get interesting as EV models from other makers (Mercedes, BMW, Renault…) garner praise when compared to Tesla’s sometimes lackadaisical finish and inattention to creature comforts.
Unless, of course, Musk’s “million robotaxis” prediction comes true — but no one took him seriously, right?
— JLG@mondaynote.com

Porsche vs Tesla: False Equivalence was originally published in Monday Note on Medium, where people are continuing the conversation by highlighting and responding to this story.
","direction":"ltr"},"title":"Porsche vs Tesla: False Equivalence","updated":1569762953218,"author":"Jean-Louis Gassée","alternate":[{"href":"https://mondaynote.com/porsche-vs-tesla-false-equivalence-29fd3b55c5b4?source=rss----c537d80ed0a---4","type":"text/html"}],"crawled":1569762953000,"published":1569762953000,"origin":{"streamId":"feed/http://www.mondaynote.com/feed/","title":"Monday Note - Medium","htmlUrl":"https://mondaynote.com?source=rss----c537d80ed0a---4"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/e31b3fcb-27f6-4f3e-b96c-53902586e366","label":"Weblogs"}]},{"id":"qxI9mPUQaH2N8pL2LRpLlzIW5hw1iiQKLdJmLkGau/I=_16d7588ab10:1f33:90d684ff","keywords":["Miscellaneous"],"originId":"http://jnack.com/blog/?p=9304","fingerprint":"b1284979","content":{"content":"Racks on racks on racks o’ cloud GPUs mean we’re setting up to deliver some awesome experiences. Check out these PM gigs:
\n\nIf you apply, please tell ‘em who sent ya. 
\n
","direction":"ltr"},"title":"PMs: Come build cloud-based gaming at Google","author":"jnack","summary":{"content":"Racks on racks on racks o’ cloud GPUs mean we’re setting up to deliver some awesome experiences. Check out these PM gigs: Product Manager, Infrastructure, Stadia – Mountain View Product Manager, Graphics, Stadia – Waterloo or Munich If you apply, please tell ‘em who sent ya. \uD83D\uDE0C","direction":"ltr"},"alternate":[{"href":"http://jnack.com/blog/2019/09/27/pms-come-build-cloud-based-gaming-at-google/","type":"text/html"}],"crawled":1569634954000,"published":1569634954000,"origin":{"streamId":"feed/http://jnack.com/blog/?feed=rss2","title":"Nackblog","htmlUrl":"http://jnack.com/blog"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/e31b3fcb-27f6-4f3e-b96c-53902586e366","label":"Weblogs"}]},{"id":"ov+64/RrrpHw8vHNs9Rbgmq2LwWdAk5I0cvaa8ohxPI=_16d74088b20:205d:90d684ff","keywords":["Technology","iOS","iOS 13","Swift Programming Language"],"originId":"https://mjtsai.com/blog/?p=26732","fingerprint":"62a7d953","content":{"content":"Alexandre Colucci (Hacker News):
\n\nThe new iOS 13 features have with no surprise been built with some Swift code in their corresponding application. This is the case of the FindMy, Reminders and Sidecar apps. Also worth noting is the use of Swift in the Health, Book and Shortcuts apps.
\n[…]
\nIf we exclude the Swift libraries, iOS 13.1 contains 141 binaries using Swift, more than doubling the number from iOS 12[…]
\n
\nPreviously:
\n","direction":"ltr"},"title":"Apple’s Use of Swift in iOS 13","author":"Michael Tsai","summary":{"content":"Alexandre Colucci (Hacker News): The new iOS 13 features have with no surprise been built with some Swift code in their corresponding application. This is the case of the FindMy, Reminders and Sidecar apps. Also worth noting is the use of Swift in the Health, Book and Shortcuts apps. […] If we exclude the Swift […]","direction":"ltr"},"alternate":[{"href":"https://mjtsai.com/blog/2019/09/27/apples-use-of-swift-in-ios-13/","type":"text/html"}],"crawled":1569609780000,"published":1569609780000,"origin":{"streamId":"feed/http://mjtsai.com/blog/feed/","title":"Michael Tsai","htmlUrl":"https://mjtsai.com/blog"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/e31b3fcb-27f6-4f3e-b96c-53902586e366","label":"Weblogs"}]},{"id":"ov+64/RrrpHw8vHNs9Rbgmq2LwWdAk5I0cvaa8ohxPI=_16d74087798:205c:90d684ff","keywords":["Technology","Artificial Intelligence","Auto-Correction","iOS","iOS 13"],"originId":"https://mjtsai.com/blog/?p=26730","fingerprint":"7a0498ce","content":{"content":"John Gruber:
\n\nOne thing I and others have noticed is that when you type a dictionary word correctly — meaning you hit the exact right keys on the on-screen keyboard — iOS 13 autocorrect will replace it with a different dictionary word that makes no contextual sense. Even beyond dictionary words, I’m seeing really strange corrections.
\n
\nI think this has been going on since before iOS 13.
\nPreviously:
\n","direction":"ltr"},"title":"iOS 13 Autocorrect Is Drunk","author":"Michael Tsai","summary":{"content":"John Gruber: One thing I and others have noticed is that when you type a dictionary word correctly — meaning you hit the exact right keys on the on-screen keyboard — iOS 13 autocorrect will replace it with a different dictionary word that makes no contextual sense. Even beyond dictionary words, I’m seeing really strange corrections. I think this […]","direction":"ltr"},"alternate":[{"href":"https://mjtsai.com/blog/2019/09/27/ios-13-autocorrect-is-drunk/","type":"text/html"}],"crawled":1569609775000,"published":1569609775000,"origin":{"streamId":"feed/http://mjtsai.com/blog/feed/","title":"Michael Tsai","htmlUrl":"https://mjtsai.com/blog"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/e31b3fcb-27f6-4f3e-b96c-53902586e366","label":"Weblogs"}]},{"id":"ov+64/RrrpHw8vHNs9Rbgmq2LwWdAk5I0cvaa8ohxPI=_16d74084ca0:205b:90d684ff","keywords":["Technology","BlockBlock","Mac","Mac App","macOS 10.14 Mojave","Malware","Security"],"originId":"https://mjtsai.com/blog/?p=26728","fingerprint":"8d8dcbfc","content":{"content":"Patrick Wardle (via Leo M):
\n\nMalware installs itself persistently, to ensure it’s automatically re-executed at reboot. BlockBlock continually monitors common persistence locations and displays an alert whenever a persistent component is added to the OS.
\n[…]
\nThis alert contains the name and path of the process that installed the persistent component, as well as details about the actual persistent component. Moreover, it shows if the process (that created the persisted item) is signed by Apple, signed by a 3rd-party, or is unsigned[…]
\n
\nIt’s the equivalent of LittleSnitch for auto-launching background processes.
\nPreviously:
\n","direction":"ltr"},"title":"BlockBlock 0.9.9.4","author":"Michael Tsai","summary":{"content":"Patrick Wardle (via Leo M): Malware installs itself persistently, to ensure it’s automatically re-executed at reboot. BlockBlock continually monitors common persistence locations and displays an alert whenever a persistent component is added to the OS. […] This alert contains the name and path of the process that installed the persistent component, as well as details […]","direction":"ltr"},"alternate":[{"href":"https://mjtsai.com/blog/2019/09/27/blockblock-0-9-9-4/","type":"text/html"}],"crawled":1569609764000,"published":1569609764000,"origin":{"streamId":"feed/http://mjtsai.com/blog/feed/","title":"Michael Tsai","htmlUrl":"https://mjtsai.com/blog"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/e31b3fcb-27f6-4f3e-b96c-53902586e366","label":"Weblogs"}]},{"id":"ov+64/RrrpHw8vHNs9Rbgmq2LwWdAk5I0cvaa8ohxPI=_16d7407eee0:205a:90d684ff","keywords":["Technology","Apple T2","Mac","macOS 10.15 Catalina","Secure Boot","Terminal"],"originId":"https://mjtsai.com/blog/?p=26726","fingerprint":"df4fbe8f","content":{"content":"Howard Oakley:
\nEven with Recovery Mode available, there have still been some reasons for wanting to enter SUM. One of them has been to run command tools to check memory for faults, as in SUM the system takes as little memory as possible, allowing you to run checks on all the rest. Sometimes fsck run from SUM could fix problems which Disk Utility in Recovery Mode couldn’t.
[…]
The biggest problem comes with Macs equipped with a T2 chip, and its Secure Boot. If it were available, a traditional SUM would bypass Secure Boot, so it isn’t allowed if your Mac has a T2 chip: Command-R will take you to Recovery, and that’s as close as your Mac can get, unless you disable Secure Boot by setting the Startup Security Utility to No Security. And you have to do that in Recovery anyway.
","direction":"ltr"},"title":"Is Single-user Mode Dead?","author":"Michael Tsai","summary":{"content":"Howard Oakley: Even with Recovery Mode available, there have still been some reasons for wanting to enter SUM. One of them has been to run command tools to check memory for faults, as in SUM the system takes as little memory as possible, allowing you to run checks on all the rest. Sometimes fsck run […]","direction":"ltr"},"alternate":[{"href":"https://mjtsai.com/blog/2019/09/27/is-single-user-mode-dead/","type":"text/html"}],"crawled":1569609740000,"published":1569609740000,"origin":{"streamId":"feed/http://mjtsai.com/blog/feed/","title":"Michael Tsai","htmlUrl":"https://mjtsai.com/blog"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/e31b3fcb-27f6-4f3e-b96c-53902586e366","label":"Weblogs"}]},{"id":"ov+64/RrrpHw8vHNs9Rbgmq2LwWdAk5I0cvaa8ohxPI=_16d7407ac78:2059:90d684ff","keywords":["Technology","App Review","App Store Rejection","App Subscriptions","Business","iOS","iOS 13","iOS App"],"originId":"https://mjtsai.com/blog/?p=26724","fingerprint":"3e02b0d2","content":{"content":"David Barnard (tweet):
\nPaying once for an app really only makes sense if the app provides minimal functionality of limited value and won’t benefit from continued improvement.
\n[…]
\nWith paid apps, people often end up buying several apps just to figure out which one best fits their needs. Let’s say they spent $3 each on 4 apps, that’s $12 they were willing to pay for a great app, but that great app they settled on only gets $3. Since most subscription apps have free trials, and many even have ad supported free tiers, people can try multiple apps and then only pay for the one they actually use.
\n[…]
\nSure, some potential customers (or existing customers if you transition from another business model to subscriptions) are going to complain about the subscription model. It’s hard to hear someone tell you that they don’t value what you’ve built, but the smart thing to do is focus on the people who are subscribing, your true fans. Figure out who they are and look for ways to reach more people like them instead of focusing on the vocal minority that complain.
\nJulian Schiavo:
\n\nMy apps been rejected with a short message basically saying ‘your app can’t use auto renewing subscriptions’, any tips/experience on this? All the other apps of this type use auto renewing subscriptions \uD83E\uDD37♂️
\n
\nReginald Braithwaite:
\n\nThe simple model I keep in my head, is that your revenue model should parallel your expense model.
\nIf they don’t, you’re both running a business AND engaging in arbitrage. That’s two things you have to get right, instead of one.
\n
\nDavid Barnard:
\n\nThis tweet reminds me of a chart by @macguru17. When you’re building an app, even one that doesn’t have ongoing costs like weather data or servers, the cost of continued development keeps putting you underwater before each update.
\nI don’t think most people realize how much most indie developers sacrifice to keep the lights on. In 11 years, I’ve probably only been cash flow positive 48 months. With paid apps, I’d build up a war chest with a big launch/update/sale, then spend it down working on the next.
\nI’ve even gone into debt to squeak by until the next big update. People seem to think all developers are rich & greedy. But it’s like any other small business. Most struggle, some do well, a few really well. But businesses don’t drop prices because they are doing well. \uD83D\uDE43
\n
\nPreviously:
\n","direction":"ltr"},"title":"Subscribers Are Your True Fans","author":"Michael Tsai","summary":{"content":"David Barnard (tweet): Paying once for an app really only makes sense if the app provides minimal functionality of limited value and won’t benefit from continued improvement. […] With paid apps, people often end up buying several apps just to figure out which one best fits their needs. Let’s say they spent $3 each on […]","direction":"ltr"},"alternate":[{"href":"https://mjtsai.com/blog/2019/09/27/subscribers-are-your-true-fans/","type":"text/html"}],"crawled":1569609723000,"published":1569609723000,"origin":{"streamId":"feed/http://mjtsai.com/blog/feed/","title":"Michael Tsai","htmlUrl":"https://mjtsai.com/blog"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/e31b3fcb-27f6-4f3e-b96c-53902586e366","label":"Weblogs"}]},{"id":"ov+64/RrrpHw8vHNs9Rbgmq2LwWdAk5I0cvaa8ohxPI=_16d74074eb8:2058:90d684ff","keywords":["Technology","iOS","iOS 12","iOS App","Twitter","Web"],"originId":"https://mjtsai.com/blog/?p=26722","fingerprint":"79c4af3b","content":{"content":"Tim Hardwick:
\nTwitter today rolled out its new “Hide Replies” feature in the U.S. and Japan, providing Twitter users with more control over the replies that are visible following a tweet.
The idea behind the feature is to give people more control over the conversations they start on the social media platform, so they can hide replies that are offensive and the hidden reply won’t show up to others as a response to the original tweet.
\nKhedron:
\n\nSo people can post false information and hide/censor anyone who calls them out on their lies or conduct.
\n
\nBen Sandofsky:
\n\nDecided to try the “Hide Reply” feature, and I noticed the hidden tweets got way more attention than if I’d just ignored them.
\nIt turns out this pop up appears when you visit my tweet.
\n
\nJuli Clover:
\n\nTwitter today implemented a feature that’s designed to allow lists of Twitter users to be pinned to the Home screen and swiped between, allowing for easy access to multiple customizable timelines.
\n
","direction":"ltr"},"title":"Twitter “Hide Replies” and Timeline Swiping","author":"Michael Tsai","summary":{"content":"Tim Hardwick: Twitter today rolled out its new “Hide Replies” feature in the U.S. and Japan, providing Twitter users with more control over the replies that are visible following a tweet.The idea behind the feature is to give people more control over the conversations they start on the social media platform, so they can hide […]","direction":"ltr"},"alternate":[{"href":"https://mjtsai.com/blog/2019/09/27/twitter-hide-replies-and-timeline-swiping/","type":"text/html"}],"crawled":1569609699000,"published":1569609699000,"origin":{"streamId":"feed/http://mjtsai.com/blog/feed/","title":"Michael Tsai","htmlUrl":"https://mjtsai.com/blog"},"unread":true,"categories":[{"id":"user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/e31b3fcb-27f6-4f3e-b96c-53902586e366","label":"Weblogs"}]}]}
\ No newline at end of file
diff --git a/Frameworks/Account/AccountTests/Feedly/TestGetCollectionsService.swift b/Frameworks/Account/AccountTests/Feedly/TestGetCollectionsService.swift
new file mode 100644
index 000000000..e9ec46176
--- /dev/null
+++ b/Frameworks/Account/AccountTests/Feedly/TestGetCollectionsService.swift
@@ -0,0 +1,26 @@
+//
+// TestGetCollectionsService.swift
+// AccountTests
+//
+// Created by Kiel Gillard on 30/10/19.
+// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
+//
+
+import XCTest
+@testable import Account
+
+final class TestGetCollectionsService: FeedlyGetCollectionsService {
+ var mockResult: Result<[FeedlyCollection], Error>?
+ var getCollectionsExpectation: XCTestExpectation?
+
+ func getCollections(completionHandler: @escaping (Result<[FeedlyCollection], Error>) -> ()) {
+ guard let result = mockResult else {
+ XCTFail("Missing mock result. Test may time out because the completion will not be called.")
+ return
+ }
+ DispatchQueue.main.async {
+ completionHandler(result)
+ self.getCollectionsExpectation?.fulfill()
+ }
+ }
+}
diff --git a/Frameworks/Account/AccountTests/Feedly/TestGetPagedStreamContentsService.swift b/Frameworks/Account/AccountTests/Feedly/TestGetPagedStreamContentsService.swift
new file mode 100644
index 000000000..0cd88b443
--- /dev/null
+++ b/Frameworks/Account/AccountTests/Feedly/TestGetPagedStreamContentsService.swift
@@ -0,0 +1,80 @@
+//
+// TestGetPagedStreamContentsService.swift
+// AccountTests
+//
+// Created by Kiel Gillard on 28/10/19.
+// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
+//
+
+import XCTest
+@testable import Account
+
+final class TestGetPagedStreamContentsService: FeedlyGetStreamContentsService {
+
+ var parameterTester: ((FeedlyResourceId, String?, Date?, Bool?) -> ())?
+ var getStreamContentsExpectation: XCTestExpectation?
+ var pages = [String: FeedlyStream]()
+
+ func addAtLeastOnePage(for resource: FeedlyResourceId, continuations: [String], numberOfEntriesPerPage count: Int) {
+ pages = [String: FeedlyStream](minimumCapacity: continuations.count + 1)
+
+ // A continuation is an identifier for the next page.
+ // The first page has a nil identifier.
+ // The last page has no next page, so the next continuation value for that page is nil.
+ // Therefore, each page needs to know the identifier of the next page.
+ for index in -1..) -> FeedlyStream {
+ let entries = range.map { index -> FeedlyEntry in
+ let content = FeedlyEntry.Content(content: "Content \(index)",
+ direction: .leftToRight)
+
+ let origin = FeedlyOrigin(title: "Origin \(index)",
+ streamId: resource.id,
+ htmlUrl: "http://localhost/feedly/origin/\(index)")
+
+ return FeedlyEntry(id: "/articles/\(index)",
+ title: "Article \(index)",
+ content: content,
+ summary: content,
+ author: nil,
+ published: Date(),
+ updated: nil,
+ origin: origin,
+ canonical: nil,
+ alternate: nil,
+ unread: true,
+ tags: nil,
+ categories: nil,
+ enclosure: nil)
+ }
+
+ let stream = FeedlyStream(id: resource.id, updated: nil, continuation: continuation, items: entries)
+
+ return stream
+ }
+
+ static func getPagingKey(for stream: FeedlyResourceId, continuation: String?) -> String {
+ return "\(stream.id)@\(continuation ?? "")"
+ }
+
+ func getStreamContents(for resource: FeedlyResourceId, continuation: String?, newerThan: Date?, unreadOnly: Bool?, completionHandler: @escaping (Result) -> ()) {
+ let key = TestGetPagedStreamContentsService.getPagingKey(for: resource, continuation: continuation)
+ guard let page = pages[key] else {
+ XCTFail("Missing page for \(resource.id) and continuation \(String(describing: continuation)). Test may time out because the completion will not be called.")
+ return
+ }
+ parameterTester?(resource, continuation, newerThan, unreadOnly)
+ DispatchQueue.main.async {
+ completionHandler(.success(page))
+ self.getStreamContentsExpectation?.fulfill()
+ }
+ }
+}
diff --git a/Frameworks/Account/AccountTests/Feedly/TestGetPagedStreamIdsService.swift b/Frameworks/Account/AccountTests/Feedly/TestGetPagedStreamIdsService.swift
new file mode 100644
index 000000000..dda3afd60
--- /dev/null
+++ b/Frameworks/Account/AccountTests/Feedly/TestGetPagedStreamIdsService.swift
@@ -0,0 +1,56 @@
+//
+// TestGetPagedStreamIdsService.swift
+// AccountTests
+//
+// Created by Kiel Gillard on 29/10/19.
+// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
+//
+
+import XCTest
+@testable import Account
+
+final class TestGetPagedStreamIdsService: FeedlyGetStreamIdsService {
+
+ var parameterTester: ((FeedlyResourceId, String?, Date?, Bool?) -> ())?
+ var getStreamIdsExpectation: XCTestExpectation?
+ var pages = [String: FeedlyStreamIds]()
+
+ func addAtLeastOnePage(for resource: FeedlyResourceId, continuations: [String], numberOfEntriesPerPage count: Int) {
+ pages = [String: FeedlyStreamIds](minimumCapacity: continuations.count + 1)
+
+ // A continuation is an identifier for the next page.
+ // The first page has a nil identifier.
+ // The last page has no next page, so the next continuation value for that page is nil.
+ // Therefore, each page needs to know the identifier of the next page.
+ for index in -1..) -> FeedlyStreamIds {
+ let entryIds = range.map { _ in UUID().uuidString }
+ let stream = FeedlyStreamIds(continuation: continuation, ids: entryIds)
+ return stream
+ }
+
+ static func getPagingKey(for stream: FeedlyResourceId, continuation: String?) -> String {
+ return "\(stream.id)@\(continuation ?? "")"
+ }
+
+ func getStreamIds(for resource: FeedlyResourceId, continuation: String?, newerThan: Date?, unreadOnly: Bool?, completionHandler: @escaping (Result) -> ()) {
+ let key = TestGetPagedStreamIdsService.getPagingKey(for: resource, continuation: continuation)
+ guard let page = pages[key] else {
+ XCTFail("Missing page for \(resource.id) and continuation \(String(describing: continuation)). Test may time out because the completion will not be called.")
+ return
+ }
+ parameterTester?(resource, continuation, newerThan, unreadOnly)
+ DispatchQueue.main.async {
+ completionHandler(.success(page))
+ self.getStreamIdsExpectation?.fulfill()
+ }
+ }
+}
diff --git a/Frameworks/Account/AccountTests/Feedly/TestGetStreamContentsService.swift b/Frameworks/Account/AccountTests/Feedly/TestGetStreamContentsService.swift
new file mode 100644
index 000000000..80bbe17ad
--- /dev/null
+++ b/Frameworks/Account/AccountTests/Feedly/TestGetStreamContentsService.swift
@@ -0,0 +1,49 @@
+//
+// TestGetStreamContentsService.swift
+// AccountTests
+//
+// Created by Kiel Gillard on 28/10/19.
+// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
+//
+
+import XCTest
+@testable import Account
+
+final class TestGetStreamContentsService: FeedlyGetStreamContentsService {
+
+ var mockResult: Result?
+ var parameterTester: ((FeedlyResourceId, String?, Date?, Bool?) -> ())?
+ var getStreamContentsExpectation: XCTestExpectation?
+
+ func getStreamContents(for resource: FeedlyResourceId, continuation: String?, newerThan: Date?, unreadOnly: Bool?, completionHandler: @escaping (Result) -> ()) {
+ guard let result = mockResult else {
+ XCTFail("Missing mock result. Test may time out because the completion will not be called.")
+ return
+ }
+ parameterTester?(resource, continuation, newerThan, unreadOnly)
+ DispatchQueue.main.async {
+ completionHandler(result)
+ self.getStreamContentsExpectation?.fulfill()
+ }
+ }
+
+ func makeMockFeedlyEntryItem() -> [FeedlyEntry] {
+ let origin = FeedlyOrigin(title: "XCTest@localhost", streamId: "user/12345/category/67890", htmlUrl: "http://localhost/nnw/xctest")
+ let content = FeedlyEntry.Content(content: "In the beginning...", direction: .leftToRight)
+ let items = [FeedlyEntry(id: "feeds/0/article/0",
+ title: "RSS Reader Ingests Man",
+ content: content,
+ summary: content,
+ author: nil,
+ published: Date(),
+ updated: nil,
+ origin: origin,
+ canonical: nil,
+ alternate: nil,
+ unread: true,
+ tags: nil,
+ categories: nil,
+ enclosure: nil)]
+ return items
+ }
+}
diff --git a/Frameworks/Account/AccountTests/Feedly/TestGetStreamIdsService.swift b/Frameworks/Account/AccountTests/Feedly/TestGetStreamIdsService.swift
new file mode 100644
index 000000000..ef35b7f53
--- /dev/null
+++ b/Frameworks/Account/AccountTests/Feedly/TestGetStreamIdsService.swift
@@ -0,0 +1,29 @@
+//
+// TestGetStreamIdsService.swift
+// AccountTests
+//
+// Created by Kiel Gillard on 29/10/19.
+// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
+//
+
+import XCTest
+@testable import Account
+
+final class TestGetStreamIdsService: FeedlyGetStreamIdsService {
+
+ var mockResult: Result?
+ var parameterTester: ((FeedlyResourceId, String?, Date?, Bool?) -> ())?
+ var getStreamIdsExpectation: XCTestExpectation?
+
+ func getStreamIds(for resource: FeedlyResourceId, continuation: String?, newerThan: Date?, unreadOnly: Bool?, completionHandler: @escaping (Result) -> ()) {
+ guard let result = mockResult else {
+ XCTFail("Missing mock result. Test may time out because the completion will not be called.")
+ return
+ }
+ parameterTester?(resource, continuation, newerThan, unreadOnly)
+ DispatchQueue.main.async {
+ completionHandler(result)
+ self.getStreamIdsExpectation?.fulfill()
+ }
+ }
+}
diff --git a/Frameworks/Account/AccountTests/Feedly/TestMarkArticlesService.swift b/Frameworks/Account/AccountTests/Feedly/TestMarkArticlesService.swift
new file mode 100644
index 000000000..1f83f2ca0
--- /dev/null
+++ b/Frameworks/Account/AccountTests/Feedly/TestMarkArticlesService.swift
@@ -0,0 +1,26 @@
+//
+// TestMarkArticlesService.swift
+// AccountTests
+//
+// Created by Kiel Gillard on 30/10/19.
+// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
+//
+
+import XCTest
+@testable import Account
+
+class TestMarkArticlesService: FeedlyMarkArticlesService {
+
+ var didMarkExpectation: XCTestExpectation?
+ var parameterTester: ((Set, FeedlyMarkAction) -> ())?
+ var mockResult: Result = .success(())
+
+ func mark(_ articleIds: Set, as action: FeedlyMarkAction, completionHandler: @escaping (Result) -> ()) {
+
+ DispatchQueue.main.async {
+ self.parameterTester?(articleIds, action)
+ completionHandler(self.mockResult)
+ self.didMarkExpectation?.fulfill()
+ }
+ }
+}
diff --git a/Frameworks/Account/AccountTests/Feedly/feedly-1-initial/collections.json b/Frameworks/Account/AccountTests/Feedly/feedly-1-initial/collections.json
new file mode 100644
index 000000000..fca4b27cc
--- /dev/null
+++ b/Frameworks/Account/AccountTests/Feedly/feedly-1-initial/collections.json
@@ -0,0 +1,1507 @@
+[
+ {
+ "customizable": true,
+ "feeds": [
+ {
+ "id": "feed/http://tidbits.com/feeds/tidbits_blurb.rss",
+ "feedId": "feed/http://tidbits.com/feeds/tidbits_blurb.rss",
+ "title": "TidBITS: Apple News for the Rest of Us",
+ "updated": 1572559519584,
+ "velocity": 15.4,
+ "subscribers": 1,
+ "website": "https://tidbits.com",
+ "language": "en",
+ "description": "Thoughtful, detailed coverage of everything Apple for 29 years\nand the TidBITS Content Network for Apple professionals"
+ },
+ {
+ "id": "feed/http://feedpress.me/sixcolors",
+ "feedId": "feed/http://feedpress.me/sixcolors",
+ "title": "Six Colors",
+ "updated": 1569955568802,
+ "velocity": 24.4,
+ "subscribers": 1,
+ "website": "https://www.sixcolors.com/",
+ "language": "en",
+ "description": "Writing about Apple and other stuff by Jason Snell, Dan Moren, and others."
+ },
+ {
+ "id": "feed/http://www.macalope.com/feed/",
+ "feedId": "feed/http://www.macalope.com/feed/",
+ "title": "Macalope",
+ "updated": 1498941877000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "http://www.macalope.com",
+ "state": "dormant",
+ "language": "en",
+ "description": "Full of sound and furry"
+ },
+ {
+ "id": "feed/http://flyingmeat.com/blog/atom.xml",
+ "feedId": "feed/http://flyingmeat.com/blog/atom.xml",
+ "title": "The Flying Meat Weblog",
+ "updated": 1343168154000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://flyingmeat.com/blog/",
+ "state": "dormant",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://www.macdrifter.com/feeds/all.atom.xml",
+ "feedId": "feed/http://www.macdrifter.com/feeds/all.atom.xml",
+ "title": "Macdrifter",
+ "updated": 1561539243000,
+ "velocity": 0.1,
+ "subscribers": 1,
+ "website": "http://www.macdrifter.com/",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://edwardfeser.blogspot.com/feeds/posts/default",
+ "feedId": "feed/http://edwardfeser.blogspot.com/feeds/posts/default",
+ "title": "Edward Feser",
+ "updated": 1572481601855,
+ "velocity": 1.1,
+ "subscribers": 1,
+ "website": "http://edwardfeser.blogspot.com/",
+ "language": "en",
+ "description": "\"One of the best contemporary writers on philosophy\" National Review\n\n\"A terrific writer\" Damian Thompson, Daily Telegraph\n\n\"Feser... has the rare and enviable gift of making philosophical argument compulsively readable\" Sir Anthony Kenny, Times Literary Supplement\n\nSelected for the First Things list of the 50 Best Blogs of 2010 (November 19, 2010)"
+ },
+ {
+ "id": "feed/http://9to5mac.com/feed/",
+ "feedId": "feed/http://9to5mac.com/feed/",
+ "title": "9to5Mac",
+ "updated": 1572574658046,
+ "velocity": 115.2,
+ "subscribers": 2,
+ "website": "https://9to5mac.com",
+ "language": "en",
+ "description": "Apple News & Mac Rumors Breaking All Day"
+ }
+ ],
+ "label": "Macintosh",
+ "created": 1569829941677,
+ "enterprise": false,
+ "numFeeds": 7,
+ "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/5ca4d61d-e55d-4999-a8d1-c3b9d8789815"
+ },
+ {
+ "customizable": false,
+ "feeds": [
+ {
+ "id": "feed/http://inessential.com/xml/rss.xml",
+ "feedId": "feed/http://inessential.com/xml/rss.xml",
+ "title": "Inessential",
+ "updated": 1569971230061,
+ "velocity": 4.5,
+ "subscribers": 1,
+ "website": "https://inessential.com/",
+ "mustRead": true,
+ "language": "en",
+ "description": "Brent Simmons\u2019s weblog."
+ }
+ ],
+ "label": "Must Read",
+ "created": 1569417923847,
+ "enterprise": false,
+ "numFeeds": 1,
+ "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.must"
+ },
+ {
+ "customizable": true,
+ "feeds": [
+ {
+ "id": "feed/http://www.codebykevin.com/blosxom.cgi/index.rss",
+ "feedId": "feed/http://www.codebykevin.com/blosxom.cgi/index.rss",
+ "title": "Code by Kevin",
+ "updated": 1564803480000,
+ "velocity": 0.1,
+ "subscribers": 1,
+ "website": "https://www.codebykevin.com/blosxom.cgi",
+ "language": "en",
+ "description": "Programming, code, business, and other pursuits"
+ },
+ {
+ "id": "feed/http://www.raywenderlich.com/feed",
+ "feedId": "feed/http://www.raywenderlich.com/feed",
+ "title": "Ray Wenderlich",
+ "updated": 1572539728140,
+ "velocity": 9.9,
+ "subscribers": 1,
+ "language": "en"
+ },
+ {
+ "id": "feed/http://ddeville.me/feed.xml",
+ "feedId": "feed/http://ddeville.me/feed.xml",
+ "title": "Damien DeVille",
+ "updated": 1454187600000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "http://ddeville.me",
+ "state": "dormant",
+ "language": "en",
+ "description": "Software engineer at Dropbox. Previously at Realmac Software. UCL Computer Science alumnus."
+ },
+ {
+ "id": "feed/http://owensd.io/rss.xml",
+ "feedId": "feed/http://owensd.io/rss.xml",
+ "title": "owensd.io - thoughts in and out - Articles",
+ "updated": 1534837458000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://owensd.io",
+ "state": "dormant",
+ "language": "en",
+ "description": "A builder of things."
+ },
+ {
+ "id": "feed/http://prog21.dadgum.com/atom.xml",
+ "feedId": "feed/http://prog21.dadgum.com/atom.xml",
+ "title": "Programming in the 21st Century",
+ "updated": 1483509600000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "http://prog21.dadgum.com/",
+ "state": "dormant",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://feeds.feedburner.com/alistapart/main",
+ "feedId": "feed/http://feeds.feedburner.com/alistapart/main",
+ "title": "A List Apart: The Full Feed",
+ "updated": 1569829877486,
+ "velocity": 0.2,
+ "subscribers": 1,
+ "website": "https://alistapart.com",
+ "language": "en",
+ "description": "Articles for people who make web sites."
+ },
+ {
+ "id": "feed/http://www.russbishop.net/feed",
+ "feedId": "feed/http://www.russbishop.net/feed",
+ "title": "Russ Bishop (atom)",
+ "updated": 1551122826000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "http://www.russbishop.net/feed",
+ "state": "dormant",
+ "language": "en",
+ "description": "This blog represents my own personal opinion and is not endorsed by my employer."
+ },
+ {
+ "id": "feed/http://mentalfaculty.tumblr.com/rss",
+ "feedId": "feed/http://mentalfaculty.tumblr.com/rss",
+ "title": "The Mental Blog",
+ "updated": 1468311749000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://mentalfaculty.tumblr.com/",
+ "state": "dormant",
+ "language": "en",
+ "description": "Drew McCormack (@drewmccormack) is founder of The Mental Faculty, developer of Mental Case and the Ensembles sync framework"
+ },
+ {
+ "id": "feed/http://blog.amyworrall.com/rss",
+ "feedId": "feed/http://blog.amyworrall.com/rss",
+ "title": "What Amy Did",
+ "updated": 1486165238000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://blog.amyworrall.com/",
+ "state": "dormant",
+ "language": "en",
+ "description": "I\u2019m a software developer from Coventry. I care about design and user experience. \n\nFollow me on Twitter: @amyruthworrall"
+ },
+ {
+ "id": "feed/http://oleb.net/blog/atom.xml",
+ "feedId": "feed/http://oleb.net/blog/atom.xml",
+ "title": "Ole Begemann: iOS Development",
+ "updated": 1559322747743,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://oleb.net/blog/",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://subjc.com/atom.xml",
+ "feedId": "feed/http://subjc.com/atom.xml",
+ "title": "Subjective-C",
+ "updated": 1461251700000,
+ "velocity": 0.0,
+ "subscribers": 2,
+ "website": "http://subjc.com/",
+ "state": "dormant",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://robnapier.net/atom.xml",
+ "feedId": "feed/http://robnapier.net/atom.xml",
+ "title": "Cocoaphony",
+ "updated": 1572285226430,
+ "velocity": 0.5,
+ "subscribers": 1,
+ "website": "https://robnapier.net/",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://cocoamanifest.net/feeds/index.xml",
+ "feedId": "feed/http://cocoamanifest.net/feeds/index.xml",
+ "title": "Cocoa Manifest",
+ "updated": 1402642440000,
+ "velocity": 0.0,
+ "subscribers": 2,
+ "website": "http://cocoamanifest.net",
+ "state": "dormant",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://petersteinberger.com/atom.xml",
+ "feedId": "feed/http://petersteinberger.com/atom.xml",
+ "title": "Peter Steinberger",
+ "updated": 1438088460000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "http://petersteinberger.com/",
+ "state": "dormant",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://www.weheartswift.com/feed/",
+ "feedId": "feed/http://www.weheartswift.com/feed/",
+ "title": "We \u2764 Swift",
+ "updated": 1569829929574,
+ "velocity": 0.2,
+ "subscribers": 1,
+ "website": "https://www.weheartswift.com",
+ "language": "en",
+ "description": "Swift Tutorials and iOS development"
+ },
+ {
+ "id": "feed/https://medium.com/feed/swift-programming",
+ "feedId": "feed/https://medium.com/feed/swift-programming",
+ "title": "Swift Programming \u2014 Medium",
+ "updated": 1554317410000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://medium.com/swift-programming?source=rss----5396e0e8bc29---4",
+ "state": "dormant",
+ "language": "en",
+ "description": "The Swift Programming Language - Medium"
+ },
+ {
+ "id": "feed/http://tonyarnold.com/atom.xml",
+ "feedId": "feed/http://tonyarnold.com/atom.xml",
+ "title": "The blog of Tony Arnold",
+ "updated": 1531312200000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://tonyarnold.com",
+ "state": "dormant",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://borkware.com/miniblog/rss/rss.xml",
+ "feedId": "feed/http://borkware.com/miniblog/rss/rss.xml",
+ "title": "Borkware Miniblog",
+ "updated": 1360542004000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://borkwarellc.wordpress.com",
+ "state": "dormant",
+ "language": "en",
+ "description": "Bork bork bork bork."
+ },
+ {
+ "id": "feed/http://nshipster.com/feed.xml",
+ "feedId": "feed/http://nshipster.com/feed.xml",
+ "title": "NSHipster",
+ "updated": 1572541874458,
+ "velocity": 0.7,
+ "subscribers": 2,
+ "website": "https://nshipster.com/",
+ "language": "en",
+ "description": "NSHipster is a journal of the overlooked bits in Objective-C, Swift, and Cocoa. Updated weekly."
+ },
+ {
+ "id": "feed/http://feeds.feedburner.com/pilkyme",
+ "feedId": "feed/http://feeds.feedburner.com/pilkyme",
+ "title": "Pilky.me",
+ "updated": 1561420800000,
+ "velocity": 0.1,
+ "subscribers": 1,
+ "website": "https://pilky.me/",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://macoscope.com/blog/feed/",
+ "feedId": "feed/http://macoscope.com/blog/feed/",
+ "title": "[macoscope blog]",
+ "updated": 1467803080000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "http://macoscope.com/blog",
+ "state": "dormant",
+ "language": "en",
+ "description": "The Macoscope Team on Designing and Developing Apps"
+ },
+ {
+ "id": "feed/http://iosunittesting.com/feed/",
+ "feedId": "feed/http://iosunittesting.com/feed/",
+ "title": "iOS Unit Testing",
+ "updated": 1436534796000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "http://iosunittesting.com",
+ "state": "dormant",
+ "language": "en",
+ "description": "It's about TDD, unit testing, and creating bug free code on iOS."
+ },
+ {
+ "id": "feed/http://airspeedvelocity.net/feed/",
+ "feedId": "feed/http://airspeedvelocity.net/feed/",
+ "title": "Airspeed Velocity",
+ "updated": 1452449244000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://airspeedvelocity.net",
+ "state": "dormant",
+ "language": "en",
+ "description": "African or European Swift?"
+ },
+ {
+ "id": "feed/http://www.cimgf.com/feed/",
+ "feedId": "feed/http://www.cimgf.com/feed/",
+ "title": "Cocoa Is My Girlfriend",
+ "updated": 1525988390000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "http://www.cimgf.com",
+ "state": "dormant",
+ "language": "en",
+ "description": "Taglines are for Windows programmers"
+ },
+ {
+ "id": "feed/http://confusatory.org/rss",
+ "feedId": "feed/http://confusatory.org/rss",
+ "title": "The Confusatory",
+ "updated": 1476982283000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://confusatory.org/",
+ "state": "dormant",
+ "language": "en",
+ "description": "cbowns\u2019s tumblr."
+ },
+ {
+ "id": "feed/http://indiestack.com/feed/",
+ "feedId": "feed/http://indiestack.com/feed/",
+ "title": "Indie Stack",
+ "updated": 1570815287712,
+ "velocity": 0.2,
+ "subscribers": 1,
+ "website": "https://indiestack.com",
+ "language": "en",
+ "description": "Hacking the Mac, iOS, and more with Daniel Jalkut"
+ }
+ ],
+ "label": "Programming",
+ "created": 1569829874442,
+ "enterprise": false,
+ "numFeeds": 26,
+ "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/885f2e01-d314-4e63-abac-17dcb063f5b5"
+ },
+ {
+ "customizable": true,
+ "feeds": [
+ {
+ "id": "feed/http://corinnekrych.blogspot.com/feeds/posts/default",
+ "feedId": "feed/http://corinnekrych.blogspot.com/feeds/posts/default",
+ "title": "chat & code",
+ "updated": 1547807580000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "http://corinnekrych.blogspot.com/",
+ "state": "dormant",
+ "language": "en",
+ "description": "Code is craft and collaboration is key to success. I love chatting the latest tech trends at coffee break: female geek."
+ },
+ {
+ "id": "feed/http://ericasadun.com/feed/",
+ "feedId": "feed/http://ericasadun.com/feed/",
+ "title": "Erica Sadun",
+ "updated": 1572197700226,
+ "velocity": 0.2,
+ "subscribers": 1,
+ "website": "https://ericasadun.com",
+ "language": "en",
+ "description": "Where technology meets something or other"
+ },
+ {
+ "id": "feed/http://therecord.co/xml/rss.xml",
+ "feedId": "feed/http://therecord.co/xml/rss.xml",
+ "title": "The Record",
+ "updated": 1401364800000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "http://therecord.co/",
+ "state": "dormant",
+ "language": "en",
+ "description": "The stories you should know about the Mac and Cocoa developer community. Hosted by Brent Simmons and Chris Parrish."
+ },
+ {
+ "id": "feed/https://grokswift.com/feed/index.xml",
+ "feedId": "feed/https://grokswift.com/feed/index.xml",
+ "title": "Grok Swift",
+ "updated": 1527175834000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://grokswift.com/",
+ "state": "dormant",
+ "language": "en"
+ },
+ {
+ "id": "feed/https://blog.alltheflow.com/rss/",
+ "feedId": "feed/https://blog.alltheflow.com/rss/",
+ "title": "All The Flow",
+ "updated": 1570032686469,
+ "velocity": 0.2,
+ "subscribers": 1,
+ "website": "http://blog.alltheflow.com/",
+ "language": "en",
+ "description": "Cocoa, Swift, tools, Auto Layout - with \ud83e\udde1"
+ },
+ {
+ "id": "feed/http://onefoottsunami.com/feed/atom/",
+ "feedId": "feed/http://onefoottsunami.com/feed/atom/",
+ "title": "One Foot Tsunami",
+ "updated": 1569938444527,
+ "velocity": 5.0,
+ "subscribers": 1,
+ "website": "https://onefoottsunami.com",
+ "language": "en",
+ "description": "Slightly less disappointing than it sounds"
+ },
+ {
+ "id": "feed/http://www.loopinsight.com/feed/",
+ "feedId": "feed/http://www.loopinsight.com/feed/",
+ "title": "Loop Insight",
+ "updated": 1572567899318,
+ "velocity": 10.6,
+ "subscribers": 1,
+ "website": "https://www.loopinsight.com",
+ "language": "en",
+ "description": "Making Sense of Technology"
+ },
+ {
+ "id": "feed/http://beckyhansmeyer.com/feed/",
+ "feedId": "feed/http://beckyhansmeyer.com/feed/",
+ "title": "Becky Hansmeyer",
+ "updated": 1569087367000,
+ "velocity": 1.4,
+ "subscribers": 1,
+ "website": "https://beckyhansmeyer.com",
+ "language": "en",
+ "description": "100% grass-fed Swift"
+ },
+ {
+ "id": "feed/http://designatednerd.com/feed/",
+ "feedId": "feed/http://designatednerd.com/feed/",
+ "title": "Designated Nerd",
+ "updated": 1564425470000,
+ "velocity": 0.1,
+ "subscribers": 1,
+ "website": "http://designatednerd.com",
+ "language": "en",
+ "description": "Software and Technical Support"
+ },
+ {
+ "id": "feed/http://appcamp4girls.com/blog?format=RSS",
+ "feedId": "feed/http://appcamp4girls.com/blog?format=RSS",
+ "title": "Blog - App Camp For Girls",
+ "updated": 1558129314000,
+ "velocity": 0.1,
+ "subscribers": 1,
+ "website": "https://appcamp4girls.com/blog/",
+ "language": "en"
+ },
+ {
+ "id": "feed/https://inspiredmouse.com/feed/",
+ "feedId": "feed/https://inspiredmouse.com/feed/",
+ "title": "Inspired Mouse",
+ "updated": 1511986957000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://inspiredmouse.com",
+ "state": "dormant",
+ "language": "en",
+ "description": "No project is too diminutive"
+ },
+ {
+ "id": "feed/https://incrementalistblog.wordpress.com/feed/",
+ "feedId": "feed/https://incrementalistblog.wordpress.com/feed/",
+ "title": "The Incrementalist.",
+ "updated": 1449808490000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://incrementalistblog.wordpress.com",
+ "state": "dormant",
+ "language": "en",
+ "description": "notes on design"
+ },
+ {
+ "id": "feed/http://www.virginiaroberts.com/feed/",
+ "feedId": "feed/http://www.virginiaroberts.com/feed/",
+ "title": "Virginia Roberts",
+ "updated": 1549571065000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "http://www.virginiaroberts.com",
+ "state": "dormant",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://jessysaurusrex.com/feed/",
+ "feedId": "feed/http://jessysaurusrex.com/feed/",
+ "title": "jessysaurusrex",
+ "updated": 1521816327000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://jessysaurusrex.com",
+ "state": "dormant",
+ "language": "en",
+ "description": "I wear big necklaces and (attempt to) hack things."
+ },
+ {
+ "id": "feed/http://blog.nicoleblee.com/feed/",
+ "feedId": "feed/http://blog.nicoleblee.com/feed/",
+ "title": "scattered thoughts",
+ "updated": 1397441482000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "http://blog.nicoleblee.com",
+ "state": "dormant",
+ "language": "en",
+ "description": "(a collection of essays and other writings)"
+ },
+ {
+ "id": "feed/https://medium.com/feed/@tessr",
+ "feedId": "feed/https://medium.com/feed/@tessr",
+ "title": "Tess Rinearson on Medium",
+ "updated": 1501016354000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://medium.com/@tessr?source=rss-c16152863954------2",
+ "state": "dormant",
+ "language": "en",
+ "description": "Stories by Tess Rinearson on Medium"
+ },
+ {
+ "id": "feed/https://medium.com/feed/@emarley",
+ "feedId": "feed/https://medium.com/feed/@emarley",
+ "title": "Liz Marley on Medium",
+ "updated": 1514047622000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://medium.com/@emarley?source=rss-b4981c59ffa5------2",
+ "state": "dormant",
+ "language": "en",
+ "description": "Stories by Liz Marley on Medium"
+ },
+ {
+ "id": "feed/http://blog.cocoabythefire.com/rss",
+ "feedId": "feed/http://blog.cocoabythefire.com/rss",
+ "title": "cocoa by the fire",
+ "updated": 1488945726000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://blog.cocoabythefire.com/",
+ "state": "dormant",
+ "language": "en",
+ "description": "Hey there, I\u2019m Brit! Coder, Entrepreneur, Daydreamer, Lucky Wife and Mom."
+ },
+ {
+ "id": "feed/http://www.catehuston.com/blog/feed/",
+ "feedId": "feed/http://www.catehuston.com/blog/feed/",
+ "title": "Accidentally in Code",
+ "updated": 1567641655000,
+ "velocity": 0.1,
+ "subscribers": 1,
+ "website": "https://cate.blog",
+ "language": "en",
+ "description": "Engineering an Interesting Life"
+ },
+ {
+ "id": "feed/http://www.aleenmean.com/feed.xml",
+ "feedId": "feed/http://www.aleenmean.com/feed.xml",
+ "title": "Aleen Mean",
+ "updated": 1562374146000,
+ "velocity": 0.1,
+ "subscribers": 1,
+ "website": "https://aleenmean.com/",
+ "language": "en",
+ "description": "Technology, diversity, and miscellaneous musings by Aleen Simms."
+ },
+ {
+ "id": "feed/http://redqueencoder.com/feed/",
+ "feedId": "feed/http://redqueencoder.com/feed/",
+ "title": "The Red Queen Coder",
+ "updated": 1569967579535,
+ "velocity": 0.2,
+ "subscribers": 1,
+ "website": "http://redqueencoder.com",
+ "language": "en",
+ "description": "If you give a person a program, you'll frustrate him for a day. If you teach a person to program, you will frustrate them for a lifetime!"
+ },
+ {
+ "id": "feed/http://meaganwaller.com/index.php/feed/",
+ "feedId": "feed/http://meaganwaller.com/index.php/feed/",
+ "title": "Meagan Waller",
+ "updated": 1403205537000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://meaganwaller.com",
+ "state": "dormant",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://www.myballard.com/feed/",
+ "feedId": "feed/http://www.myballard.com/feed/",
+ "title": "Ballard",
+ "updated": 1572553245909,
+ "velocity": 13.8,
+ "subscribers": 1,
+ "website": "https://www.myballard.com",
+ "language": "en",
+ "description": "News, events and restaurants for Seattle's Ballard and Fremont neighborhoods"
+ },
+ {
+ "id": "feed/https://kateheddleston.com/blog/feed.atom",
+ "feedId": "feed/https://kateheddleston.com/blog/feed.atom",
+ "title": "KateHeddleston.com Blog Posts",
+ "updated": 1526573156000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://www.kateheddleston.com/blog",
+ "state": "dormant",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://blog.erynwells.me/rss",
+ "feedId": "feed/http://blog.erynwells.me/rss",
+ "title": "Eryn Wells",
+ "updated": 1442175535000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://blog.erynwells.me/",
+ "state": "dormant",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://lambdamaphone.blogspot.com/feeds/posts/default",
+ "feedId": "feed/http://lambdamaphone.blogspot.com/feeds/posts/default",
+ "title": "Everything in Context",
+ "updated": 1519599000002,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "http://lambdamaphone.blogspot.com/",
+ "state": "dormant",
+ "language": "en",
+ "description": "Game design, programming languages, and academia."
+ },
+ {
+ "id": "feed/http://nothe.purplellamas.net/index.xml",
+ "feedId": "feed/http://nothe.purplellamas.net/index.xml",
+ "title": "Blog Posts About Stuff",
+ "updated": 1512518400000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "http://nothe.purplellamas.net/",
+ "state": "dormant",
+ "language": "en",
+ "description": "Recent content on Blog Posts About Stuff"
+ },
+ {
+ "id": "feed/http://www.mostgood.net/blog?format=RSS",
+ "feedId": "feed/http://www.mostgood.net/blog?format=RSS",
+ "title": "mostgood",
+ "updated": 1433114183000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "http://www.mostgood.net/",
+ "state": "dormant",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://www.mistys-internet.website/blog/atom.xml",
+ "feedId": "feed/http://www.mistys-internet.website/blog/atom.xml",
+ "title": "The Future Is Now",
+ "updated": 1550722972000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "http://mistys-internet.website/blog/",
+ "state": "dormant",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://pewpewthespells.com/feed.xml",
+ "feedId": "feed/http://pewpewthespells.com/feed.xml",
+ "title": "Samantha Marshall's Blog",
+ "updated": 1569829870614,
+ "velocity": 8.6,
+ "subscribers": 1,
+ "website": "https://pewpewthespells.com/",
+ "language": "en",
+ "description": "Blog Feed"
+ },
+ {
+ "id": "feed/http://blog.ashleynh.me/rss/",
+ "feedId": "feed/http://blog.ashleynh.me/rss/",
+ "title": "Ashley Nelson-Hornstein",
+ "updated": 1496606482000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "http://ashleynh.me:80/",
+ "state": "dormant",
+ "language": "en",
+ "description": "Ashley Nelson"
+ },
+ {
+ "id": "feed/https://medium.com/feed/@nerdonica",
+ "feedId": "feed/https://medium.com/feed/@nerdonica",
+ "title": "Veronica Ray on Medium",
+ "updated": 1471184706000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://medium.com/@nerdonica?source=rss-eaf18ccd367f------2",
+ "state": "dormant",
+ "language": "en",
+ "description": "Stories by Veronica Ray on Medium"
+ },
+ {
+ "id": "feed/http://www.nadynerichmond.com/blog/feed/",
+ "feedId": "feed/http://www.nadynerichmond.com/blog/feed/",
+ "title": "go ahead, mac my day",
+ "updated": 1506105463000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "http://www.nadynerichmond.com/blog",
+ "state": "dormant",
+ "language": "en",
+ "description": "a Macintosh girl in a Microsoft world"
+ },
+ {
+ "id": "feed/http://www.bbc.co.uk/blogs/doctorwho/rss",
+ "feedId": "feed/http://www.bbc.co.uk/blogs/doctorwho/rss",
+ "title": "Doctor Who",
+ "updated": 1570021053044,
+ "velocity": 0.2,
+ "subscribers": 1,
+ "website": "https://www.bbc.co.uk/blogs/doctorwho",
+ "language": "en",
+ "description": "All the latest news and features from the world of Doctor Who."
+ },
+ {
+ "id": "feed/http://scripting.com/rss.xml",
+ "feedId": "feed/http://scripting.com/rss.xml",
+ "title": "Scripting News",
+ "updated": 1569956453445,
+ "velocity": 15.4,
+ "subscribers": 1,
+ "website": "http://scripting.com/",
+ "language": "en",
+ "description": "Scripting News, the weblog started in 1994 that bootstrapped the blogging revolution. \ud83d\ude80"
+ },
+ {
+ "id": "feed/http://www.mechanicalgirl.com/feeds/all/",
+ "feedId": "feed/http://www.mechanicalgirl.com/feeds/all/",
+ "title": "MechanicalGirl",
+ "updated": 1569829801167,
+ "velocity": 0.2,
+ "subscribers": 1,
+ "website": "http://www.MechanicalGirl.com/",
+ "language": "en",
+ "description": "Latest posts on MechanicalGirl"
+ },
+ {
+ "id": "feed/http://ranchero.com/xml/rss.xml",
+ "feedId": "feed/http://ranchero.com/xml/rss.xml",
+ "title": "ranchero.com",
+ "updated": 1572554111531,
+ "velocity": 4.3,
+ "subscribers": 1,
+ "website": "https://inessential.com/",
+ "language": "en",
+ "description": "Brent Simmons\u2019s weblog."
+ },
+ {
+ "id": "feed/http://natashatherobot.com/feed/",
+ "feedId": "feed/http://natashatherobot.com/feed/",
+ "title": "Natasha The Robot",
+ "updated": 1569829929101,
+ "velocity": 0.2,
+ "subscribers": 2,
+ "website": "https://www.natashatherobot.com",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://daringfireball.net/index.xml",
+ "feedId": "feed/http://daringfireball.net/index.xml",
+ "title": "Daring Fireball",
+ "updated": 1572566913757,
+ "velocity": 27.5,
+ "subscribers": 6,
+ "website": "https://daringfireball.net/",
+ "language": "en",
+ "description": "By John Gruber"
+ },
+ {
+ "id": "feed/http://timekl.com/atom.xml",
+ "feedId": "feed/http://timekl.com/atom.xml",
+ "title": "don't panic",
+ "updated": 1555225200000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://timekl.com/",
+ "state": "dormant",
+ "language": "en",
+ "description": "Occasional posts, usually about technology"
+ },
+ {
+ "id": "feed/http://nataliepo.typepad.com/nataliepo/rss.xml",
+ "feedId": "feed/http://nataliepo.typepad.com/nataliepo/rss.xml",
+ "title": "nataliepo (posts on 'nataliepo' (rss 2.0))",
+ "updated": 1447256454000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://nataliepo.typepad.com/nataliepo/",
+ "state": "dormant",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://shapeof.com/rss.xml",
+ "feedId": "feed/http://shapeof.com/rss.xml",
+ "title": "The Shape of Everything",
+ "updated": 1569263479000,
+ "velocity": 1.6,
+ "subscribers": 1,
+ "website": "https://shapeof.com/",
+ "language": "en",
+ "description": "A website mostly about Mac stuff, written by Gus Mueller"
+ },
+ {
+ "id": "feed/http://jvns.ca/atom.xml",
+ "feedId": "feed/http://jvns.ca/atom.xml",
+ "title": "Julia Evans",
+ "updated": 1572365556732,
+ "velocity": 1.1,
+ "subscribers": 2,
+ "website": "http://jvns.ca",
+ "language": "en"
+ },
+ {
+ "id": "feed/https://www.natashatherobot.com/feed/",
+ "feedId": "feed/https://www.natashatherobot.com/feed/",
+ "title": "Natasha the Robot",
+ "updated": 1545498499000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://www.natashatherobot.com",
+ "state": "dormant",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://pointersgonewild.com/feed/",
+ "feedId": "feed/http://pointersgonewild.com/feed/",
+ "title": "Pointers Gone Wild",
+ "updated": 1560168135000,
+ "velocity": 0.1,
+ "subscribers": 1,
+ "website": "https://pointersgonewild.com",
+ "language": "en",
+ "description": "A blog about compilers, programming and technology."
+ },
+ {
+ "id": "feed/http://www.kristinathai.com/feed/",
+ "feedId": "feed/http://www.kristinathai.com/feed/",
+ "title": "kristinathai.com",
+ "updated": 1563831802000,
+ "velocity": 0.1,
+ "subscribers": 1,
+ "website": "http://www.kristinathai.com",
+ "language": "ja"
+ },
+ {
+ "id": "feed/https://developer.apple.com/swift/blog/news.rss",
+ "feedId": "feed/https://developer.apple.com/swift/blog/news.rss",
+ "title": "Swift Blog - Apple Developer",
+ "updated": 1476306000000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://developer.apple.com/swift/blog/",
+ "state": "dormant",
+ "language": "en",
+ "description": "Get the latest news and helpful tips on the Swift programming language from the engineers who created it."
+ },
+ {
+ "id": "feed/http://www.rebeccamiller-webster.com/feed/",
+ "feedId": "feed/http://www.rebeccamiller-webster.com/feed/",
+ "title": "Rebecca Miller-Webster",
+ "updated": 1547836736000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://www.rebeccamiller-webster.com",
+ "state": "dormant",
+ "language": "en",
+ "description": "Ruby + JavaScript"
+ },
+ {
+ "id": "feed/http://swift.ayaka.me/posts?format=RSS",
+ "feedId": "feed/http://swift.ayaka.me/posts?format=RSS",
+ "title": "Learn Swift \u21af",
+ "updated": 1466314290000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "http://swift.ayaka.me/",
+ "state": "dormant",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://www.imore.com/rss.xml",
+ "feedId": "feed/http://www.imore.com/rss.xml",
+ "title": "iMore",
+ "updated": 1570675375343,
+ "velocity": 17.8,
+ "subscribers": 1,
+ "website": "https://www.imore.com/",
+ "language": "en",
+ "description": "More news and rumors, more help and how-tos, more app and accessory reviews, more iPhone and iPad and iPod touch. More of everything you love. iMore."
+ },
+ {
+ "id": "feed/http://blog.thoughtbrain.com/feed/",
+ "feedId": "feed/http://blog.thoughtbrain.com/feed/",
+ "title": "Feed: Thoughtbrain Bloggers",
+ "updated": 1426140251000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "http://blog.thoughtbrain.com",
+ "state": "dormant",
+ "language": "en",
+ "description": "Designery nerdy things."
+ },
+ {
+ "id": "feed/http://blog.ellenchisa.com/feed/",
+ "feedId": "feed/http://blog.ellenchisa.com/feed/",
+ "title": "Ellen's Blog",
+ "updated": 1546640740000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://blog.ellenchisa.com?source=rss----da542b929da2---4",
+ "state": "dormant",
+ "language": "en",
+ "description": "I\u2019m starting a new company with @paulbiggar, which you can learn about at https://darklang.com. I mostly write about startups and software development. - Medium"
+ },
+ {
+ "id": "feed/https://medium.com/feed/@jaimeejaimee",
+ "feedId": "feed/https://medium.com/feed/@jaimeejaimee",
+ "title": "jaimeejaimee",
+ "updated": 1558376026000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://medium.com/@jaimeejaimee?source=rss-11d5cc4494a2------2",
+ "language": "en",
+ "description": "Stories by jaimeejaimee on Medium"
+ }
+ ],
+ "label": "THree",
+ "created": 1569829699432,
+ "enterprise": false,
+ "numFeeds": 53,
+ "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8"
+ },
+ {
+ "customizable": true,
+ "feeds": [
+ {
+ "id": "feed/http://bryan.io/rss",
+ "feedId": "feed/http://bryan.io/rss",
+ "title": "bryan i/o",
+ "updated": 1567230243000,
+ "velocity": 0.2,
+ "subscribers": 1,
+ "website": "https://bryan.io/",
+ "language": "en",
+ "description": "Software engineer who led iOS at Tumblr from 2012-2015. Mostly cheeseburgers at this point. Over at irace.me nowadays."
+ },
+ {
+ "id": "feed/http://nickbradbury.com/feed/",
+ "feedId": "feed/http://nickbradbury.com/feed/",
+ "title": "Nick Bradbury",
+ "updated": 1503946502000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://nickbradbury.com",
+ "state": "dormant",
+ "language": "en",
+ "description": "I develop Android apps. In a previous life I created HomeSite, TopStyle and FeedDemon for Windows."
+ },
+ {
+ "id": "feed/http://feeds.feedburner.com/domainofthebored",
+ "feedId": "feed/http://feeds.feedburner.com/domainofthebored",
+ "title": "Peter Hosey",
+ "updated": 1567916255000,
+ "velocity": 0.1,
+ "subscribers": 1,
+ "website": "https://boredzo.org/blog",
+ "language": "en",
+ "description": "The personal weblog of Peter Hosey."
+ },
+ {
+ "id": "feed/http://blog.metaobject.com/feeds/posts/default",
+ "feedId": "feed/http://blog.metaobject.com/feeds/posts/default",
+ "title": "metablog",
+ "updated": 1556396880001,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://blog.metaobject.com/",
+ "state": "dormant",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://feeds2.feedburner.com/adobe/jnack",
+ "feedId": "feed/http://feeds2.feedburner.com/adobe/jnack",
+ "title": "John Nack on Adobe (rss (feedburner))",
+ "updated": 1391820146000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "http://blogs.adobe.com/jnack",
+ "state": "dormant",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://www.zathras.de/angelweb/BlogRSSFeed.rss",
+ "feedId": "feed/http://www.zathras.de/angelweb/BlogRSSFeed.rss",
+ "title": "Zathras.de - Uli's most useless blog in the World",
+ "updated": 1562364000000,
+ "velocity": 0.1,
+ "subscribers": 1,
+ "website": "https://orangejuiceliberationfront.com/",
+ "language": "en",
+ "description": "Uli's blog on programming, game development, pop culture and other boring things."
+ },
+ {
+ "id": "feed/http://typesetinthefuture.com/feed/",
+ "feedId": "feed/http://typesetinthefuture.com/feed/",
+ "title": "Typeset In The Future",
+ "updated": 1544536375000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://typesetinthefuture.com",
+ "state": "dormant",
+ "language": "en",
+ "description": "Typography and Design in Science Fiction Movies"
+ },
+ {
+ "id": "feed/http://david-smith.org/atom.xml",
+ "feedId": "feed/http://david-smith.org/atom.xml",
+ "title": "David Smith",
+ "updated": 1570631341711,
+ "velocity": 0.2,
+ "subscribers": 1,
+ "website": "http://david-smith.org/",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://awkwardhare.com/rss",
+ "feedId": "feed/http://awkwardhare.com/rss",
+ "title": "Awkward Hare",
+ "updated": 1484149569000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://awkwardhare.com/",
+ "state": "dormant",
+ "language": "en",
+ "description": "A blog by Greg Pierce"
+ },
+ {
+ "id": "feed/http://frozendevil.com/atom.xml",
+ "feedId": "feed/http://frozendevil.com/atom.xml",
+ "title": "frozendevil",
+ "updated": 1398927600000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "http://frozendevil.com/",
+ "state": "dead",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://useyourloaf.com/blog/rss.xml",
+ "feedId": "feed/http://useyourloaf.com/blog/rss.xml",
+ "title": "Use Your Loaf",
+ "updated": 1569830765949,
+ "velocity": 0.5,
+ "subscribers": 2,
+ "website": "https://useyourloaf.com/blog/",
+ "language": "en",
+ "description": "Recent content on Use Your Loaf - iOS Development News & Tips"
+ },
+ {
+ "id": "feed/http://www.appleoutsider.com/feed/",
+ "feedId": "feed/http://www.appleoutsider.com/feed/",
+ "title": "Apple Outsider",
+ "updated": 1402413759000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://www.appleoutsider.com",
+ "state": "dormant",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://rathole.tumblr.com/rss",
+ "feedId": "feed/http://rathole.tumblr.com/rss",
+ "title": "RatHole",
+ "updated": 1554476924000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://rathole.tumblr.com/",
+ "state": "dormant",
+ "language": "en",
+ "description": "what my brain does when I\u2019m not looking"
+ },
+ {
+ "id": "feed/http://codeplease.io/rss/",
+ "feedId": "feed/http://codeplease.io/rss/",
+ "title": "Codeplease",
+ "updated": 1511101649000,
+ "velocity": 0.2,
+ "subscribers": 1,
+ "website": "http://codeplease.io/",
+ "state": "dead",
+ "language": "en",
+ "description": "Ramblings about code"
+ },
+ {
+ "id": "feed/http://blog.jaredsinclair.com/rss?1",
+ "feedId": "feed/http://blog.jaredsinclair.com/rss?1",
+ "title": "Jared Sinclair",
+ "updated": 1554679244000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://jaredsinclair.com/",
+ "state": "dormant",
+ "language": "en",
+ "description": "Write an awesome description for your new site here. You can edit this line in _config.yml. It will appear in your document head meta (for Google search results) and in your feed.xml site description."
+ },
+ {
+ "id": "feed/http://www.takingnotes.co/atom.xml",
+ "feedId": "feed/http://www.takingnotes.co/atom.xml",
+ "title": "Doug Russell",
+ "updated": 1541721600000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "http://takingnotes.co//",
+ "state": "dormant",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://www.red-sweater.com/blog/feed",
+ "feedId": "feed/http://www.red-sweater.com/blog/feed",
+ "title": "Red Sweater",
+ "updated": 1568998271000,
+ "velocity": 0.5,
+ "subscribers": 1,
+ "website": "https://red-sweater.com/blog",
+ "language": "en",
+ "description": "Official blog of Red Sweater Software"
+ },
+ {
+ "id": "feed/http://mjtsai.com/blog/feed/",
+ "feedId": "feed/http://mjtsai.com/blog/feed/",
+ "title": "Michael Tsai",
+ "updated": 1569964972836,
+ "velocity": 18.7,
+ "subscribers": 1,
+ "website": "https://mjtsai.com/blog",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://jnack.com/blog/?feed=rss2",
+ "feedId": "feed/http://jnack.com/blog/?feed=rss2",
+ "title": "Nackblog",
+ "updated": 1569939043384,
+ "velocity": 2.7,
+ "subscribers": 1,
+ "website": "http://jnack.com/blog",
+ "language": "en",
+ "description": "Musings on photography, illustration, mobile apps, and more"
+ },
+ {
+ "id": "feed/http://bitsplitting.org/feed/",
+ "feedId": "feed/http://bitsplitting.org/feed/",
+ "title": "Daniel Jalkut",
+ "updated": 1570348471161,
+ "velocity": 0.2,
+ "subscribers": 1,
+ "website": "https://bitsplitting.org",
+ "language": "en",
+ "description": "Chasing the impossible with Daniel Jalkut"
+ },
+ {
+ "id": "feed/http://stmts.net/feed/",
+ "feedId": "feed/http://stmts.net/feed/",
+ "title": "Jesper",
+ "updated": 1298233430000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "http://stmts.net",
+ "state": "dormant",
+ "language": "en",
+ "description": "On programming"
+ },
+ {
+ "id": "feed/http://www.mikeash.com/pyblog/rss.py",
+ "feedId": "feed/http://www.mikeash.com/pyblog/rss.py",
+ "title": "NSBlog",
+ "updated": 1570800820984,
+ "velocity": 0.2,
+ "subscribers": 2,
+ "website": "http://www.mikeash.com/pyblog/",
+ "language": "en",
+ "description": "Mac OS X and Cocoa programming"
+ },
+ {
+ "id": "feed/http://corporationunknown.com/blog/feed/",
+ "feedId": "feed/http://corporationunknown.com/blog/feed/",
+ "title": "Corporation Unknown",
+ "updated": 1420427842000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "http://corporationunknown.com/blog",
+ "state": "dormant",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://jamesdempsey.net/feed/",
+ "feedId": "feed/http://jamesdempsey.net/feed/",
+ "title": "James Dempsey",
+ "updated": 1568304414000,
+ "velocity": 0.2,
+ "subscribers": 1,
+ "website": "https://jamesdempsey.net",
+ "language": "en",
+ "description": "From Apple to Indie in three easy steps"
+ },
+ {
+ "id": "feed/http://brian-webster.tumblr.com/rss",
+ "feedId": "feed/http://brian-webster.tumblr.com/rss",
+ "title": "Very Web. Such Blog. Wow.",
+ "updated": 1530051926000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://brian-webster.tumblr.com/",
+ "state": "dormant",
+ "language": "en",
+ "description": "Brian Webster\u2019s sporadic blogging about mostly programming stuff."
+ },
+ {
+ "id": "feed/http://dangillmor.com/feed/",
+ "feedId": "feed/http://dangillmor.com/feed/",
+ "title": "Dan Gillmor",
+ "updated": 1565895640000,
+ "velocity": 0.1,
+ "subscribers": 1,
+ "website": "http://dangillmor.com",
+ "language": "en",
+ "description": "Just in case you were still wondering\u2026"
+ },
+ {
+ "id": "feed/http://www.jeffmcleman.com/blog/feed/",
+ "feedId": "feed/http://www.jeffmcleman.com/blog/feed/",
+ "title": "Jeff McLeman",
+ "updated": 1563565996000,
+ "velocity": 0.1,
+ "subscribers": 1,
+ "website": "https://www.jeffmcleman.com/blog",
+ "language": "en",
+ "description": "The Brooding Thoughts of an Untamed Mind"
+ },
+ {
+ "id": "feed/http://www.caseyliss.com/rss",
+ "feedId": "feed/http://www.caseyliss.com/rss",
+ "title": "Liss is More",
+ "updated": 1571061991343,
+ "velocity": 0.9,
+ "subscribers": 1,
+ "website": "https://www.caseyliss.com",
+ "language": "en",
+ "description": "Posts to Liss is More"
+ },
+ {
+ "id": "feed/http://ignorethecode.net/blog/rss/",
+ "feedId": "feed/http://ignorethecode.net/blog/rss/",
+ "title": "ignorethecode.net",
+ "updated": 1570434952848,
+ "velocity": 0.2,
+ "subscribers": 1,
+ "website": "http://ignorethecode.net",
+ "language": "en",
+ "description": "Essays on usability, programming, and other nerd topics."
+ },
+ {
+ "id": "feed/http://sheilasweblog.wordpress.com/feed/",
+ "feedId": "feed/http://sheilasweblog.wordpress.com/feed/",
+ "title": "Sheila's Weblog",
+ "updated": 1237602766000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "https://sheilasweblog.wordpress.com",
+ "state": "dormant",
+ "language": "en",
+ "description": "Quilting, kitties, other fun stuff."
+ },
+ {
+ "id": "feed/http://www.allenpike.com/feed/",
+ "feedId": "feed/http://www.allenpike.com/feed/",
+ "title": "Allen Pike",
+ "updated": 1571074705991,
+ "velocity": 0.5,
+ "subscribers": 1,
+ "website": "https://www.allenpike.com/",
+ "language": "en"
+ },
+ {
+ "id": "feed/https://developer.apple.com/news/rss/news.rss",
+ "feedId": "feed/https://developer.apple.com/news/rss/news.rss",
+ "title": "iPhone Developer News",
+ "updated": 1571840736737,
+ "velocity": 1.1,
+ "subscribers": 1,
+ "website": "https://developer.apple.com/news/",
+ "language": "en",
+ "description": "Apple Developer News and Updates feed provided by Apple, Inc."
+ },
+ {
+ "id": "feed/http://themainthread.com/feed.xml",
+ "feedId": "feed/http://themainthread.com/feed.xml",
+ "title": "The Main Thread",
+ "updated": 1440820800000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "http://themainthread.com/",
+ "state": "dormant",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://www.gordonmeyer.com/atom.xml",
+ "feedId": "feed/http://www.gordonmeyer.com/atom.xml",
+ "title": "Gordon Meyer (posts on 'gordon meyer' (atom))",
+ "updated": 1569066494000,
+ "velocity": 0.5,
+ "subscribers": 1,
+ "website": "https://www.gordonmeyer.com/",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://www.mondaynote.com/feed/",
+ "feedId": "feed/http://www.mondaynote.com/feed/",
+ "title": "Monday Note",
+ "updated": 1572263376500,
+ "velocity": 1.4,
+ "subscribers": 1,
+ "website": "https://mondaynote.com?source=rss----c537d80ed0a---4",
+ "language": "en",
+ "description": "Media, Tech, Business Models viewed from Palo Alto and Paris - Medium"
+ },
+ {
+ "id": "feed/http://www.randsinrepose.com/index.xml",
+ "feedId": "feed/http://www.randsinrepose.com/index.xml",
+ "title": "Rands In Repose",
+ "updated": 1569830107339,
+ "velocity": 0.2,
+ "subscribers": 4,
+ "website": "https://randsinrepose.com",
+ "language": "en"
+ },
+ {
+ "id": "feed/http://furbo.org/feed/",
+ "feedId": "feed/http://furbo.org/feed/",
+ "title": "furbo.org",
+ "updated": 1569830050733,
+ "velocity": 0.5,
+ "subscribers": 1,
+ "website": "https://furbo.org",
+ "language": "en",
+ "description": "by Craig Hockenberry"
+ },
+ {
+ "id": "feed/http://feeds.feedburner.com/NSHipster",
+ "feedId": "feed/http://feeds.feedburner.com/NSHipster",
+ "title": "NSHipster",
+ "updated": 1572543860464,
+ "velocity": 0.7,
+ "subscribers": 1,
+ "website": "https://nshipster.com/",
+ "language": "en",
+ "description": "NSHipster is a journal of the overlooked bits in Objective-C, Swift, and Cocoa. Updated weekly."
+ },
+ {
+ "id": "feed/http://www.neglectedpotential.com/feed/",
+ "feedId": "feed/http://www.neglectedpotential.com/feed/",
+ "title": "Neglected Potential",
+ "updated": 1537811051000,
+ "velocity": 0.0,
+ "subscribers": 1,
+ "website": "http://www.neglectedpotential.com",
+ "state": "dormant",
+ "language": "en"
+ }
+ ],
+ "label": "Weblogs",
+ "created": 1569829952574,
+ "enterprise": false,
+ "numFeeds": 39,
+ "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/e31b3fcb-27f6-4f3e-b96c-53902586e366"
+ },
+ {
+ "customizable": true,
+ "feeds": [
+ {
+ "id": "feed/http://edwardfeser.blogspot.com/feeds/posts/default",
+ "feedId": "feed/http://edwardfeser.blogspot.com/feeds/posts/default",
+ "title": "Edward Feser",
+ "updated": 1572481601855,
+ "velocity": 1.1,
+ "subscribers": 1,
+ "website": "http://edwardfeser.blogspot.com/",
+ "language": "en",
+ "description": "\"One of the best contemporary writers on philosophy\" National Review\n\n\"A terrific writer\" Damian Thompson, Daily Telegraph\n\n\"Feser... has the rare and enviable gift of making philosophical argument compulsively readable\" Sir Anthony Kenny, Times Literary Supplement\n\nSelected for the First Things list of the 50 Best Blogs of 2010 (November 19, 2010)"
+ }
+ ],
+ "label": "\ud83e\udd1e\ud83c\udffb\ud83e\udd1e\ud83c\udffb\ud83e\udd1e\ud83c\udffb",
+ "created": 1570099138718,
+ "enterprise": false,
+ "numFeeds": 1,
+ "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/fbdcd69b-7e27-4b6a-bfed-6584b944155d"
+ },
+ {
+ "customizable": false,
+ "feeds": [
+ {
+ "id": "feed/http://inessential.com/xml/rss.xml",
+ "feedId": "feed/http://inessential.com/xml/rss.xml",
+ "title": "Inessential",
+ "updated": 1569971230061,
+ "velocity": 4.5,
+ "subscribers": 1,
+ "website": "https://inessential.com/",
+ "mustRead": true,
+ "language": "en",
+ "description": "Brent Simmons\u2019s weblog."
+ }
+ ],
+ "label": "Uncategorized",
+ "enterprise": false,
+ "numFeeds": 1,
+ "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.uncategorized"
+ }
+]
diff --git a/Frameworks/Account/AccountTests/Feedly/feedly-1-initial/global.all.json b/Frameworks/Account/AccountTests/Feedly/feedly-1-initial/global.all.json
new file mode 100644
index 000000000..493a8d84c
--- /dev/null
+++ b/Frameworks/Account/AccountTests/Feedly/feedly-1-initial/global.all.json
@@ -0,0 +1,40811 @@
+{
+ "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/global.all",
+ "updated": 1572574658046,
+ "continuation": "16d97ded573:516:53b826a2",
+ "items": [
+ {
+ "keywords": [
+ "Apple"
+ ],
+ "originId": "https://9to5mac.com/?p=617831",
+ "fingerprint": "3c886abd",
+ "id": "BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16e24c0fdfe:1f026:d4506071",
+ "author": "Michael Steeber",
+ "summary": {
+ "direction": "ltr",
+ "content": "\nApple\u2019s collaboration with major cultural centers around the world continues this fall in Spain through a partnership with CaixaForum Madrid. The museum will host Today at Apple sessions teaching creative skills amplified by the center\u2019s architecture and exhibitions.
\n more\u2026
\nThe post Today at Apple and CaixaForum collaborate on creative sessions in Madrid appeared first on 9to5Mac.
"
+ },
+ "alternate": [
+ {
+ "href": "https://9to5mac.com/2019/10/31/today-at-apple-caixaforum-madrid-sessions/",
+ "type": "text/html"
+ }
+ ],
+ "crawled": 1572574658046,
+ "title": "Today at Apple and CaixaForum collaborate on creative sessions in Madrid",
+ "published": 1572573184000,
+ "origin": {
+ "streamId": "feed/http://9to5mac.com/feed/",
+ "htmlUrl": "https://9to5mac.com",
+ "title": "9to5Mac"
+ },
+ "visual": {
+ "url": "http://www.blogcdn.com/www.engadget.com/media/2013/10/nvidia-shield-console-mode.jpg",
+ "width": 620,
+ "height": 340,
+ "contentType": "image/jpg"
+ },
+ "unread": true,
+ "categories": [
+ {
+ "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/5ca4d61d-e55d-4999-a8d1-c3b9d8789815",
+ "label": "Macintosh"
+ }
+ ]
+ },
+ {
+ "keywords": [
+ "Apple"
+ ],
+ "originId": "https://9to5mac.com/?p=617827",
+ "fingerprint": "e914881a",
+ "id": "BmoAzSEWHFzR01wyxBZAhNEo11Vy8oDR1qKDe+tKVEQ=_16e248a079d:1ef6c:d4506071",
+ "author": "Chance Miller",
+ "summary": {
+ "direction": "ltr",
+ "content": "\nApple has made several changes at the executive level recently. Bloomberg reports today that Apple has promoted a handful of employees to the vice president level, which is described as \u201ca key title reserved for the most influential players.\u201d
\n more\u2026
\nThe post Apple promotes several senior directors to VP positions, early iPhone marketing exec returns appeared first on 9to5Mac.
"
+ },
+ "alternate": [
+ {
+ "href": "https://9to5mac.com/2019/10/31/apple-vice-president-changes-promotions/",
+ "type": "text/html"
+ }
+ ],
+ "crawled": 1572571056029,
+ "title": "Apple promotes several senior directors to VP positions, early iPhone marketing exec returns",
+ "published": 1572569880000,
+ "origin": {
+ "streamId": "feed/http://9to5mac.com/feed/",
+ "htmlUrl": "https://9to5mac.com",
+ "title": "9to5Mac"
+ },
+ "unread": true,
+ "categories": [
+ {
+ "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/5ca4d61d-e55d-4999-a8d1-c3b9d8789815",
+ "label": "Macintosh"
+ }
+ ]
+ },
+ {
+ "keywords": [
+ "Uncategorized"
+ ],
+ "originId": "http://www.loopinsight.com/?p=100513",
+ "fingerprint": "5446932a",
+ "id": "A4VVV8iwf2HD21qpAa7ABDI8MY8lm8dIbsVbE6sVKNA=_16e2459dcb6:1eea2:d4506071",
+ "author": "Shawn King",
+ "summary": {
+ "direction": "ltr",
+ "content": "It's all going to come down to content and price.\u221e Read this on The Loop"
+ },
+ "alternate": [
+ {
+ "href": "https://www.vox.com/recode/2019/10/31/20941114/apple-disney-hbo-peacock-streaming-tv-explained",
+ "type": "text/html"
+ }
+ ],
+ "crawled": 1572567899318,
+ "title": "Your guide to Apple TV+, Disney+, and all the other streamers chasing Netflix",
+ "published": 1572564396000,
+ "origin": {
+ "streamId": "feed/http://www.loopinsight.com/feed/",
+ "htmlUrl": "https://www.loopinsight.com",
+ "title": "The Loop"
+ },
+ "content": {
+ "direction": "ltr",
+ "content": "It\u2019s all going to come down to content and price.
\n\u221e Read this on The Loop
"
+ },
+ "unread": true,
+ "categories": [
+ {
+ "id": "user/f2f031bd-f3e3-4893-a447-467a291c6d1e/category/66132046-6f14-488d-b590-8e93422723c8",
+ "label": "THree"
+ }
+ ]
+ },
+ {
+ "originId": "tag:daringfireball.net,2019:/linked//6.36213",
+ "fingerprint": "6ceae7b2",
+ "id": "ZTHt7g74IlVC5A2IgEvcn/aop5teo99gzFaGU2TCGxs=_16e244ad2dd:1ee46:d4506071",
+ "updated": 1572566423000,
+ "author": "John Gruber",
+ "alternate": [
+ {
+ "href": "https://twitter.com/marcoarment/status/1189899504562122752",
+ "type": "text/html"
+ }
+ ],
+ "crawled": 1572566913757,
+ "title": "iOS 13.2 Is Overzealously Killing Apps in the Background",
+ "published": 1572566422000,
+ "origin": {
+ "streamId": "feed/http://daringfireball.net/index.xml",
+ "htmlUrl": "https://daringfireball.net/",
+ "title": "Daring Fireball"
+ },
+ "content": {
+ "direction": "ltr",
+ "content": "Marco Arment, on Twitter:
\n\nMajor new bugs introduced in iOS 13.2:
\n